Pages

Pages#

Pages in Duxt are Dart components that render at specific routes. They can define layouts, middleware, SEO metadata, and load data asynchronously.

Basic Page#

// lib/home/pages/index.dart
import 'package:jaspr/jaspr.dart';

class HomePage extends StatelessComponent {
  @override
  Component build(BuildContext context) {
    return div([
      h1([text('Welcome to Duxt')]),
      p([text('Build full-stack Dart web apps.')]),
    ]);
  }
}

DuxtPage#

Extend DuxtPage for advanced features:

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

class DashboardPage extends DuxtPage {
  @override
  String? get layout => 'dashboard';

  @override
  List<String> get middleware => ['auth'];

  @override
  String? get title => 'Dashboard';

  @override
  Map<String, String> get meta => {
    'description': 'Your personal dashboard',
    'og:title': 'Dashboard | MyApp',
  };

  @override
  bool get requiresAuth => true;

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

DuxtPage API#

PropertyTypeDescription
layoutString?Layout name to wrap the page
middleware List<String> Middleware to run before rendering
titleString?Page title for SEO
metaMap<String, String>Meta tags for SEO
requiresAuth bool If true, requires authentication

Async Data Loading#

Use AsyncDataMixin to load data on the server before rendering:

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

class PostPage extends DuxtPage with AsyncDataMixin {
  Map<String, dynamic>? post;

  @override
  Future<void> asyncData(DuxtContext context) async {
    final id = context.params['id'];
    post = await Api.get('/posts/$id');
  }

  @override
  Component build(BuildContext context) {
    if (post == null) {
      return div([text('Loading...')]);
    }

    return article([
      h1([text(post!['title'])]),
      p([text(post!['content'])]),
    ]);
  }
}

DuxtContext#

The context provides route information and control flow:

class DuxtContext {
  final String path;                  // Current path
  final Map<String, String> params;   // Route params (/posts/_id_ → {id: '123'})
  final Map<String, String> query;    // Query params (?page=1 → {page: '1'})
  final Map<String, dynamic> state;   // Shared state

  void redirect(String path);         // Redirect to another route
  void error(int code, String msg);   // Return an error
}

Accessing Parameters#

@override
Future<void> asyncData(DuxtContext context) async {
  // Route: /posts/_id_
  // URL: /posts/123
  final postId = context.params['id'];  // '123'

  // URL: /posts/123?comments=true
  final showComments = context.query['comments'];  // 'true'
}

Redirecting#

@override
Future<void> asyncData(DuxtContext context) async {
  final post = await Api.get('/posts/${context.params['id']}');

  if (post == null) {
    context.redirect('/404');
    return;
  }

  this.post = post;
}

Returning Errors#

@override
Future<void> asyncData(DuxtContext context) async {
  final user = await AuthService.getCurrentUser();

  if (user == null) {
    context.error(401, 'Please log in to view this page');
    return;
  }
}

Page with Layout#

class BlogPostPage extends DuxtPage {
  @override
  String? get layout => 'blog';  // Uses BlogLayout

  @override
  Component build(BuildContext context) {
    return article([
      h1([text('My Blog Post')]),
      p([text('Content here...')]),
    ]);
  }
}

Page with Middleware#

class AdminPage extends DuxtPage {
  @override
  List<String> get middleware => ['auth', 'admin'];

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

SEO Metadata#

class AboutPage extends DuxtPage {
  @override
  String? get title => 'About Us | MyApp';

  @override
  Map<String, String> get meta => {
    'description': 'Learn about our company and team.',
    'keywords': 'about, company, team',
    'og:title': 'About Us',
    'og:description': 'Learn about our company and team.',
    'og:image': '/images/about-og.png',
    'twitter:card': 'summary_large_image',
  };

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

Authentication Required#

class ProfilePage extends DuxtPage {
  @override
  bool get requiresAuth => true;  // Redirects to login if not authenticated

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

Dynamic Routes#

Single Parameter#

// lib/posts/pages/_id_.dart → /posts/:id
class PostPage extends DuxtPage with AsyncDataMixin {
  @override
  Future<void> asyncData(DuxtContext context) async {
    final id = context.params['id'];
    // Load post by ID
  }
}

Multiple Parameters#

// lib/users/pages/_userId_/posts/_postId_.dart → /users/:userId/posts/:postId
class UserPostPage extends DuxtPage with AsyncDataMixin {
  @override
  Future<void> asyncData(DuxtContext context) async {
    final userId = context.params['userId'];
    final postId = context.params['postId'];
  }
}

Catch-All Routes#

// lib/docs/pages/_...slug_.dart → /docs/*
class DocsPage extends DuxtPage with AsyncDataMixin {
  @override
  Future<void> asyncData(DuxtContext context) async {
    final slug = context.params['slug'];  // 'guides/routing' or 'api/reference'
  }
}

Error Handling#

Handle errors gracefully with try-catch:

class PostPage extends DuxtPage with AsyncDataMixin {
  Map<String, dynamic>? post;
  String? error;

  @override
  Future<void> asyncData(DuxtContext context) async {
    try {
      post = await Api.get('/posts/${context.params['id']}');
    } catch (e) {
      error = 'Failed to load post';
    }
  }

  @override
  Component build(BuildContext context) {
    if (error != null) {
      return div([text(error!)]);
    }

    if (post == null) {
      return div([text('Loading...')]);
    }

    return article([
      h1([text(post!['title'])]),
    ]);
  }
}