Server-Side Rendering & Hybrid Rendering

AngularDart supports Server-Side Rendering (SSR) and Hybrid Rendering, allowing you to render your application on the server before sending HTML to the client. This dramatically improves initial page load performance, SEO, and social media sharing while keeping all the interactivity of a full SPA on the client side through hydration.

Table of Contents

  1. What is Server-Side Rendering?
  2. Why SSR Matters for AngularDart
  3. How It Works: The Complete Flow
  4. Architecture Overview
  5. Creating a Project with SSR
  6. Project Structure
  7. Understanding Each File
  8. Building and Running
  9. Render Modes
  10. Hybrid Rendering in Detail
  11. TransferState: Sharing Data Between Server and Client
  12. Server-Side Routing
  13. Building a Custom SSR Server
  14. Advanced: Per-Component Render Mode Configuration
  15. Prerendering vs Real-Time SSR
  16. Common Patterns and Best Practices
  17. Troubleshooting

What is Server-Side Rendering?

When you build a traditional web application (like an AngularDart SPA), the browser downloads an almost empty HTML page containing only a few tags like <app-component></app-component>. The browser then downloads JavaScript, executes it, and only after that does your content appear on screen. This is called Client-Side Rendering (CSR).

Server-Side Rendering (SSR) flips this model: the server runs your AngularDart application, generates the full HTML with all content already rendered, and sends that complete HTML to the browser. The user sees the page immediately without waiting for JavaScript to download and execute.

Think of it like ordering food at a restaurant:

  • CSR (Client-Side): You get raw ingredients and have to cook everything yourself. The kitchen is empty until you start cooking.
  • SSR: The chef has already cooked your meal. It's ready the moment you sit down. When you arrive at the table, the food is there hot and complete.

Why SSR Matters for AngularDart

1. Faster First Paint

With CSR, users see a blank screen while JavaScript downloads, parses, and executes. With SSR, content appears instantly because the HTML is already rendered. This is especially important for:

  • Users on slow mobile networks (3G/4G)
  • Devices with limited processing power (budget phones, tablets)
  • Large applications with heavy JavaScript bundles

2. Better SEO (Search Engine Optimization)

While modern search engines like Google can execute JavaScript and index SPA content, it's not always reliable or immediate. With SSR:

  • Search engine crawlers see fully rendered HTML exactly what users see
  • Content is indexed immediately, without waiting for JavaScript execution
  • Meta tags, headings, and structured data are present in the initial HTML

3. Better Social Media Sharing

When you share a link on social media (Twitter, Facebook, LinkedIn), these platforms fetch your page to generate preview cards with titles, descriptions, and images. Without SSR:

  • The crawler sees an empty <app-component></app-component> tag
  • No title, no description, no image just a bare URL

With SSR, the crawler gets fully rendered HTML with all meta tags populated.

4. Progressive Enhancement

SSR doesn't replace your SPA. After the server-rendered HTML loads, AngularDart hydrates it attaching event listeners and making the page interactive. The user sees content immediately (from SSR), then the app becomes fully interactive (via hydration). This gives you the best of both worlds: fast initial load + rich interactivity.


How It Works: The Complete Flow

Let's walk through what happens from the moment a user types your URL to the moment they're interacting with your app.

Step 1: User Requests a Page

User browser -> GET /about -> Your Server

The browser sends an HTTP request for /about to your server.

Step 2: Server Renders the Component

Your server (running Dart on the VM) receives the request and tells AngularDart to render your application component but instead of rendering to DOM (which doesn't exist on the server), it renders to an HTML string.

// On the server, running in Dart VM:
final html = await server.renderApplication(
  ng.appComponentFactory,
  url: request.uri.toString(),
);
// html is now a complete HTML document with your content!

Step 3: Server Sends Complete HTML

The server sends back a full HTML document:

<!DOCTYPE html>
<html lang="en" ng-server-context="ssr">
<head>
  <meta charset="UTF-8">
  <title>About - My App</title>
</head>
<body>
  <app-root>
    <h1>About Us</h1>
    <p>We are a company that builds great software.</p>
  </app-root>
</body>
</html>

The user sees this content immediately no blank screen, no loading spinner.

Step 4: Browser Loads JavaScript and Hydrates

Meanwhile (or after), the browser loads your AngularDart JavaScript bundle. Instead of creating all DOM elements from scratch (which would duplicate work already done by the server), it hydrates the existing HTML:

  1. It detects that the page was server-rendered (ng-server-context="ssr" attribute)
  2. It scans the existing DOM for special markers (data-ng-id) placed during SSR
  3. It reuses those existing elements instead of creating new ones
  4. It attaches event listeners and binds data to the reused elements

The result: content appears instantly (from SSR), then becomes interactive (via hydration). No flash of unstyled content, no duplicate rendering.

Visual Summary

Time ->
-----------------------------------------------►

Server Side:                    Client Side:
+--------------+               +--------------------------+
| Receive      |               | Receive HTML             |
| Request      |               | (content visible!)       |
+--------------+               +--------------------------+
| Render to    |               | Download JS bundle       |
| HTML string  |-------------> | (while content shows)    |
+--------------+               +--------------------------+
| Send full    |<------------- | Parse & execute JS       |
| HTML         |   HTTP        |                          |
+--------------+               +--------------------------+
                               | Hydrate: attach events  |
                               | to existing DOM          |
                               +--------------------------+

User sees content immediately -> App becomes fully interactive

Architecture Overview

AngularDart's SSR implementation is built on several key abstractions that work together seamlessly.

The RenderNode Abstraction

The core innovation in AngularDart's SSR architecture is the RenderNode abstraction. This is what makes it possible to use the same component code for both server and client rendering.

                  +------------------+
                  |  RenderNode      |  <- Abstract interface
                  |  (interface)     |
                  +--------+---------+
                           |
              +------------+------------+
              |                         |
   +----------v----------+  +----------v----------+
   | BrowserRenderNode   |  | ServerRenderNode   |
   |                     |  |                    |
   | Wraps web.Element   |  | Builds HTML string |
   | Uses real DOM       |  | Uses StringBuffer  |
   |                     |  |                    |
   | appendChild(el)     |  | appendChild(html)  |
   | setProperty()       |  | setProperty()      |
   | setText()           |  | setText()          |
   +---------------------+  +--------------------+

The template compiler generates code that uses RenderNode operations. At runtime:

  • On the client, BrowserRenderNode wraps real DOM elements (web.Element)
  • On the server, ServerRenderNode builds an HTML string using Dart's StringBuffer

This means your component templates are compiled to the same code regardless of where they run. The difference is purely in the runtime implementation.

The RenderFactory

The RenderFactory is a singleton that creates RenderNode instances. It switches between browser and server modes:

// Enable server mode (called by platformServer)
renderFactory.useServerMode();

// Create elements this will use ServerRenderNode on the server,
// BrowserRenderNode on the client
final node = renderFactory.createElement('div', parent);
node.setText('Hello World');

// Restore browser mode after rendering
renderFactory.useBrowserMode();

Hydration Engine

The hydration engine is what makes SSR truly efficient. Instead of throwing away the server-rendered HTML and rebuilding everything from scratch on the client, it:

  1. Marks every DOM element with a unique data-ng-id attribute during SSR
  2. On the client, scans the existing DOM and builds a lookup map of IDs to elements
  3. When the component tree is built, reuses existing elements instead of creating new ones
  4. Attaches event listeners and data bindings to the reused elements

This eliminates the "double render" problem that plagued early SSR implementations (like AngularJS's ngServerSide).


Creating a Project with SSR

The easiest way to create an AngularDart project with SSR support is using the CLI. There are three SSR-related flags:

Flag What it creates
--ssr Minimal SSR scaffold: server entry point, HTTP server, hydration detection. No routing.
--hybrid Full SSR + routing + RenderMode control + TransferState, with components decoupled in lib/.
--ssr --seo SSR + routing + SEO (angulardart_seo), with components in lib/.

Minimal SSR project

ngdart new my_ssr_app --ssr
cd my_ssr_app
dart pub get

The --ssr flag:

  • Adds angulardart_server to your dependencies
  • Creates a server entry point (web/main.server.dart)
  • Creates an HTTP server (bin/server.dart)
  • Writes a hydration-aware web/main.dart (the root component is inline in this minimal scaffold)
  • Creates the conditional platform-DOM files (lib/platform_dom.dart, lib/platform_dom_browser.dart, lib/platform_dom_vm.dart)
  • Configures build.yaml for dual compilation (client + server)

Hybrid project (routing + RenderMode)

For projects that need routing and per-component render mode control, use the --hybrid flag:

ngdart new my_hybrid_app --hybrid
cd my_hybrid_app
dart pub get

The --hybrid flag creates components in lib/ (app_component.dart, home_component.dart, about_component.dart, dashboard_component.dart) with different RenderMode settings (server, client, automatic) plus a data_service.dart that demonstrates TransferState. --hybrid implies SSR, so it cannot be combined with --ssr.

SSR + SEO project

ngdart new my_ssr_seo_app --ssr --seo
cd my_ssr_seo_app
dart pub get

This combines SSR with angulardart_seo (dynamic meta tags, Open Graph, Twitter Cards) and angulardart_prerender for static prerendering. See the SEO Guide for details.

Adding SSR to an Existing Project

You can also add SSR support to an existing AngularDart project:

cd my_existing_app
ngdart add ssr        # or: ngdart add hybrid
dart pub get

These commands validate your project, modify pubspec.yaml, build.yaml, and index.html, create web/main.server.dart, bin/server.dart, and the conditional platform import files. They are idempotent safe to run multiple times.


Project Structure

Standard SSR Project (--ssr)

A project created with --ssr has this structure:

my_ssr_app/
├── bin/
│   └── server.dart              # HTTP server (dart:io + angulardart_server)
├── lib/
│   ├── platform_dom.dart        # Conditional export (browser | VM)
│   ├── platform_dom_browser.dart # Re-exports dart:html on the web
│   └── platform_dom_vm.dart     # No-op stubs for the Dart VM
├── web/
│   ├── index.html               # HTML shell with ng-client-context="csr"
│   ├── app_component.html       # Root component template
│   ├── main.dart                # Client entry point (with hydration detection)
│   ├── main.server.dart         # Server entry point (exports appComponentFactory)
│   ├── main.template.dart       # Generated by build_runner + generate-stubs
│   └── main.server.template.dart # Generated server bundle
├── pubspec.yaml                 # Dependencies include angulardart_server
├── build.yaml                   # Two entrypoints: web/main.dart + web/main.server.dart
└── analysis_options.yaml

Hybrid Project Structure (--hybrid)

A project created with --hybrid has components in lib/ with per-component render modes:

my_hybrid_app/
├── bin/
│   └── server.dart              # HTTP server (dart:io + angulardart_server)
├── lib/
│   ├── platform_dom.dart        # Conditional export (browser | VM)
│   ├── platform_dom_browser.dart # Browser-side DOM APIs
│   ├── platform_dom_vm.dart     # VM-side stubs for AOT compilation
│   ├── app_component.dart       # Root component with routing
│   ├── home_component.dart      # RenderMode.server  SEO-critical content
│   ├── about_component.dart     # RenderMode.automatic (default)
│   ├── dashboard_component.dart # RenderMode.client  user-specific data
│   └── data_service.dart        # @Injectable() with TransferState usage
├── web/
│   ├── index.html               # HTML shell with ng-client-context="csr" and <base href="/">
│   ├── main.dart                # Client entry point (with hydration detection)
│   ├── main.server.dart         # Server entry point (exports appComponentFactory + appInjector)
│   └── styles.css               # Global styles
├── pubspec.yaml                 # Dependencies include angulardart_server, angulardart_router
└── build.yaml                   # Two entrypoints: web/main.dart + web/main.server.dart

Key Files Explained

File Purpose
bin/server.dart The HTTP server that handles requests and renders AngularDart components to HTML
web/main.server.dart Server entry point exports the component factory (and injector, for routing)
web/main.dart Client entry point detects if page was SSR'd and hydrates, or runs normally
pubspec.yaml Contains angulardart_server >=1.2.0 <2.0.0 dependency
lib/platform_dom*.dart Conditional browser/VM DOM APIs required for AOT compilation on both platforms

Example: Complete SSR Project

A complete working example demonstrating all SSR features (hybrid rendering, TransferState, routing, reactive forms) is available in the repository at angular/examples/ssr_full/. It includes:

  • Home component (RenderMode.server) with TransferState.set() for SEO-critical content
  • About component (RenderMode.automatic) with encapsulated styles
  • Contact form (reactive forms via angulardart_forms)
  • Dashboard component (RenderMode.client) with interactive counter

Understanding Each File

The Server Entry Point (web/main.server.dart)

For a minimal --ssr project:

import 'package:angulardart/angulardart.dart';
// ignore: uri_has_not_been_generated
import 'main.template.dart' as ng;

/// Returns the root component factory for server-side rendering.
ComponentFactory<Object> get appComponentFactory =>
    ng.AppComponentNgFactory;

For a --hybrid project with routing, it also exports the application injector:

import 'package:angulardart/angulardart.dart';
import 'package:angulardart_router/angulardart_router.dart';
import 'package:angulardart_server/angulardart_server.dart';
// ignore: uri_has_not_been_generated
import 'package:my_app/app_component.template.dart' as app;
// ignore: uri_has_not_been_generated
import 'main.server.template.dart' as ng;

ComponentFactory<Object> get appComponentFactory =>
    app.AppComponentNgFactory;

/// Application injector (routing) for server-side rendering.
@GenerateInjector([
  routerProviders,
  ClassProvider(PlatformLocation, useClass: ServerPlatformLocation),
  ValueProvider.forToken(appBaseHref, '/'),
])
final InjectorFactory appInjector = ng.appInjector$Injector;

The // ignore: uri_has_not_been_generated comments suppress analyzer warnings because the .template.dart files are generated by build_runner (and copied into the source tree by ngdart generate-stubs) they don't exist in version control.

The HTTP Server (bin/server.dart)

import 'dart:async';
import 'dart:io';

import 'package:angulardart_server/angulardart_server.dart';
// ignore: uri_has_not_been_generated
import '../web/main.server.dart' as ng;

Future<void> main() async {
  final server = platformServer();

  await HttpServer.bind('localhost', 4000).then((httpServer) {
    print('AngularDart SSR server running on http://localhost:4000');

    httpServer.listen((request) async {
      final path = request.uri.path;

      // Serve static assets (JS, CSS, images) from the build output.
      if (_isStaticAsset(path)) {
        await _serveStatic(request);
        return;
      }

      try {
        final html = await server.renderApplication(
          ng.appComponentFactory,
          url: request.uri.toString(),
        );

        request.response
          ..headers.contentType = ContentType.html
          ..write(html)
          ..close();
      } catch (e, st) {
        print('Render error: $e');
        print(st);
        request.response
          ..statusCode = HttpStatus.internalServerError
          ..write('<h1>Server Error</h1>')
          ..close();
      }
    });
  });
}

This is a standard Dart HTTP server using dart:io. Here's what happens for each request:

  1. platformServer() creates the SSR platform instance (singleton)
  2. server.renderApplication(...) renders your AngularDart component tree to a complete HTML string, including <html>, <head>, and <body> tags
  3. The url parameter tells AngularDart what URL was requested, so the router can match routes correctly (important for server-side routing)
  4. Static assets (compiled main.dart.js, CSS, images) are served directly from disk; everything else is server-rendered

Note about routing: for a project that uses the router, you must also pass a parentInjector that provides appBaseHref (see Server-Side Routing).

The Client Entry Point (web/main.dart)

import 'package:angulardart/angulardart.dart';
import 'package:angulardart_server/angulardart_server.dart';
import 'package:my_app/platform_dom.dart' as platform_dom;
// ignore: uri_has_not_been_generated
import 'main.template.dart' as ng;

void main() async {
  // Detect if the page was server-rendered
  final isServerRendered =
      (platform_dom.window as dynamic).document.documentElement?.getAttribute('ng-server-context') == 'ssr';

  if (isServerRendered) {
    // Hydrate: reuse existing DOM from SSR
    await hydrateApplication(ng.AppComponentNgFactory);
  } else {
    // Normal client-side rendering
    runApp(ng.AppComponentNgFactory);
  }
}

This is the most critical file for understanding how hydration works. The key logic:

  1. Detection: It checks if the <html> element has ng-server-context="ssr". This attribute is automatically added by renderApplication() in the server HTML wrapper.

  2. Hydration path (hydrateApplication): If the page was SSR'd, AngularDart reuses the existing DOM elements instead of creating new ones. Event listeners and data bindings are attached to the pre-existing elements.

  3. Normal rendering path (runApp): If no SSR was detected (e.g., direct access to index.html, or a client-only request), it renders normally as a standard SPA.

Key insight: The same main.dart file works for both SSR and non-SSR scenarios. The platform_dom.dart conditional export (export 'platform_dom_browser.dart' if (dart.library.io) 'platform_dom_vm.dart') re-exports dart:html on the web and no-op stubs on the VM, so the file compiles on both platforms. No conditional compilation needed just runtime detection.


Building and Running

This is the build flow, and it differs from a plain CSR project in two important ways.

The Build Commands

dart pub get
dart run build_runner clean
dart run build_runner build web --release
ngdart generate-stubs
dart bin/server.dart

Then open http://localhost:4000 in your browser.

Why --release?

build_runner build in dev mode compiles with the Dart Dev Compiler (DDC). The resulting main.dart.js is only a bootstrap that loads the actual modules (require.js, stack_trace_mapper.dart.js, ...) from /packages/.... Those are only served by build_runner serve, so a standalone bin/server.dart would return 404 for them.

--release compiles with dart2js instead, producing a self-contained main.dart.js that the standalone server can serve directly. Always use --release for the SSR build.

Why ngdart generate-stubs?

bin/server.dart runs on the Dart VM (native Dart, not JavaScript). It imports web/main.server.dart, which in turn imports the generated .template.dart files. build_runner writes those files under .dart_tool/build/generated/<package>/, but the Dart VM resolves package: imports to lib/ and relative imports to web/.

ngdart generate-stubs copies the generated .template.dart (and .css.shim.dart) files from .dart_tool/build/generated/<package>/ into your web/ and lib/ directories, so the VM can find them. It also creates safe stub templates and strips browser-only imports. Run it after every build.


Render Modes

AngularDart supports three render modes that you can specify per component:

enum RenderMode {
  /// Server-side rendering + client hydration
  server,

  /// Client-side only (not rendered on the server)
  client,

  /// Automatic: SSR on server, CSR on client
  automatic,
}

Using renderMode in Components

You specify the render mode as a parameter to the @Component decorator:

// This component will always be rendered on the server
@Component(
  selector: 'hero-section',
  template: '<h1>Welcome</h1>',
  renderMode: RenderMode.server,
)
class HeroSection {}

// This component is client-only (not rendered on server at all)
@Component(
  selector: 'user-dashboard',
  template: '<p>User-specific content</p>',
  renderMode: RenderMode.client,
)
class UserDashboard {}

// Default behavior automatic detection
@Component(
  selector: 'footer',
  template: '<p>© 2025 My App</p>',
  // renderMode defaults to RenderMode.automatic
)
class Footer {}

When to Use Each Mode

Mode Best For Example
RenderMode.server SEO-critical content, marketing pages, blog posts Home page, About page, Product pages
RenderMode.client User-specific/dynamic content, authenticated areas Dashboard, user profile, shopping cart
RenderMode.automatic (default) Shared components that benefit from SSR when available Navigation bar, footer, common widgets

Server-Side Behavior by Render Mode

When a component has renderMode: RenderMode.client, the server doesn't render its content. Instead, it outputs an empty placeholder tag:

<!-- Server renders this for client-only components -->
<user-dashboard></user-dashboard>

The browser then fills in the actual content via client-side rendering during hydration or normal app startup. This is useful for:

  • Performance: Avoid rendering expensive components on the server
  • Correctness: Some features (like window access, event listeners) only work on the client
  • Security: User-specific data should never be shared in server-rendered HTML

Hybrid Rendering in Detail

Hybrid Rendering means mixing different render modes within the same application. Some pages or components are rendered on the server, while others are rendered exclusively on the client all in a single request/response cycle.

Example: A Page with Mixed Render Modes

Imagine an e-commerce site where:

  • The product listing page is SSR'd (for SEO)
  • The user's shopping cart is CSR-only (personalized, dynamic data)
  • The navigation bar uses automatic mode (works in both contexts)
@Component(
  selector: 'product-page',
  template: '<nav-bar></nav-bar>'
      '<h1>{{productName}}</h1>'
      '<p>{{productDescription}}</p>'
      '<shopping-cart></shopping-cart>',
  directives: [NavBar, ShoppingCart],
)
class ProductPage {}

@Component(
  selector: 'nav-bar',
  template: '<a href="/">Home</a>',
  renderMode: RenderMode.automatic, // SSR when available
)
class NavBar {}

@Component(
  selector: 'shopping-cart',
  template: '<h2>Your Cart</h2>'
      '<ul><li *ngFor="let item of items">'
      '{{item.name}} x{{item.quantity}} - ${{item.price}}</li></ul>',
  renderMode: RenderMode.client, // Never SSR'd user-specific data
)
class ShoppingCart {}

When the server renders ProductPage:

  1. <nav-bar> is rendered as HTML (because it's automatic/SSR-capable)
  2. <shopping-cart> outputs an empty placeholder tag (<shopping-cart></shopping-cart>)
  3. The rest of the product content is SSR'd

On the client:

  1. The nav bar is hydrated from existing DOM
  2. The shopping cart is rendered fresh by the client (since it wasn't in server HTML)
  3. Everything becomes interactive through hydration + normal binding

Why Hybrid Rendering Matters

Without hybrid rendering, you face a trade-off:

  • All SSR: Fast SEO, but wasted effort rendering user-specific content that changes immediately on the client
  • All CSR: Good for dynamic apps, but poor SEO and slow initial load

Hybrid rendering lets you choose the right approach for each part of your application.


TransferState: Sharing Data Between Server and Client

A common challenge with SSR is sharing data fetched on the server with the client. Without TransferState, you'd make the same API call twice once on the server (during rendering) and again on the client (after hydration).

TransferState solves this by serializing data into a <script> tag in the HTML, which the client reads after hydration.

Server-Side: Storing Data

import 'package:angulardart_server/angulardart_server.dart';

// Fetch data on the server during rendering
final users = await http.get('https://api.example.com/users').then(
  (response) => jsonDecode(response.body) as List,
);

// Store it in TransferState this will be serialized into the HTML
TransferState.set<List>('users', users);

// Now render the component it can access this data via TransferState.get()
final html = await server.renderComponent(AppComponentNgFactory);

Client-Side: Reading Data

import 'package:angulardart_server/angulardart_server.dart';

void main() async {
  final isServerRendered =
      (platform_dom.window as dynamic).document.documentElement?.getAttribute('ng-server-context') == 'ssr';

  if (isServerRendered) {
    await hydrateApplication(AppComponentNgFactory);

    // After hydration, read the transferred state
    final users = TransferState.get<List>('users');
    print('Received ${users?.length} users from server!');
  } else {
    runApp(AppComponentNgFactory);
  }
}

How It Works Under the Hood

On the server, TransferState.toScript() generates a <script> tag:

<script id="ng-transfer-state" type="application/json">
{"ng-transfer-state:users":[{"name":"Alice"},{"name":"Bob"}]}
</script>

This script is automatically injected into the HTML by renderApplication(). On the client, TransferState.fromHtml() (called automatically by hydrateApplication) reads this script tag and populates the in-memory state map.

What Types Are Supported?

TransferState supports any data that can be serialized to JSON:

  • String, int, double, bool
  • List<T> where T is serializable
  • Map<String, dynamic> with serializable values
  • Nested combinations of the above

Non-serializable objects are serialized using their JSON encoding (falling back to a placeholder string).


Server-Side Routing

To use angulardart_router together with SSR, the router must be able to resolve the requested URL on the server (where there is no window/document).

Client side

Your root component injects Router, declares its routes, and renders a <router-outlet>:

import 'package:angulardart/angulardart.dart';
import 'package:angulardart_router/angulardart_router.dart';
// ignore: uri_has_not_been_generated
import 'home_component.template.dart' as home;
// ignore: uri_has_not_been_generated
import 'about_component.template.dart' as about;

@Component(
  selector: 'app-root',
  templateUrl: 'app_component.html',
  directives: [routerDirectives],
)
class AppComponent implements OnInit {
  final Router _router;
  List<RouteDefinition> routes = [];

  AppComponent(this._router);

  @override
  void ngOnInit() {
    _router.onRouteActivated.listen((_) {});
    routes = [
      RouteDefinition(path: '/', component: home.HomeComponentNgFactory, useAsDefault: true),
      RouteDefinition(path: '/about', component: about.AboutComponentNgFactory),
    ];
  }
}
<!-- app_component.html -->
<nav>
  <a [routerLink]="['/']" routerLinkActive="active">Home</a> |
  <a [routerLink]="['/about']" routerLinkActive="active">About</a>
</nav>
<main><router-outlet [routes]="routes"></router-outlet></main>

And provide the router providers at the root:

@GenerateInjector([routerProviders])
final InjectorFactory appInjector = ng.appInjector$Injector;

// Pass it to bootstrap:
runApp(app.AppComponentNgFactory, createInjector: appInjector);
// ...and to hydration:
await hydrateApplication(app.AppComponentNgFactory, createInjector: appInjector);

Server side

On the server, renderApplication automatically injects a ServerPlatformLocation that reads the URL from the HTTP request. But PathLocationStrategy also needs an appBaseHref (there is no <base href> element on the VM). Provide it via the parentInjector:

// bin/server.dart
final baseHrefInjector = Injector.map({appBaseHref: '/'});

final html = await server.renderApplication(
  ng.appComponentFactory,
  url: request.uri.toString(),
  parentInjector: ng.appInjector(baseHrefInjector),
);

And make sure web/main.server.dart exports appInjector with routerProviders (see The Server Entry Point).

Static asset fallback for routes

Because your SPA routes (/about, /products/42) don't correspond to files on disk, the server must treat any non-asset path as a route to render (which the template bin/server.dart already does). For a production deployment behind a reverse proxy, you may also need to rewrite unknown paths to the server (see Deployment Guide).


Building a Custom SSR Server

While the basic server example uses dart:io's HttpServer, you can build more sophisticated servers using any Dart HTTP framework. Here's an example with shelf (Google's HTTP middleware library):

import 'dart:async';
import 'dart:io';

import 'package:angulardart_server/angulardart_server.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf_static/shelf_static.dart' as static_file_handler;
// ignore: uri_has_not_been_generated
import '../web/main.server.dart' as ng;

Future<void> main() async {
  final server = platformServer();

  // Serve static assets (JS, CSS, images) from the build output directory.
  final staticHandler = static_file_handler.createStaticHandler('build/web');

  await HttpServer.bind('localhost', 4000).then((httpServer) {
    httpServer.listen((request) async {
      final path = request.uri.path;

      // Let static files pass through (JS, CSS, images).
      if (_isStaticAsset(path)) {
        final response = await staticHandler(request.uri.path);
        if (response != null) {
          request.response.statusCode = response.statusCode;
          await request.response.addStream(response.read());
          await request.response.close();
          return;
        }
      }

      // SSR for all other routes.
      try {
        final html = await server.renderApplication(
          ng.appComponentFactory,
          url: request.uri.toString(),
        );
        request.response
          ..headers.contentType = ContentType.html
          ..write(html)
          ..close();
      } catch (e) {
        request.response
          ..statusCode = HttpStatus.internalServerError
          ..write('<h1>Server Error</h1>')
          ..close();
      }
    });
  });
}

bool _isStaticAsset(String path) {
  return path.endsWith('.js') ||
      path.endsWith('.css') ||
      path.endsWith('.png') ||
      path.endsWith('.jpg') ||
      path.endsWith('.ico') ||
      path.contains('/assets/');
}

This server:

  1. Serves static assets (JS, CSS, images) directly from the file system
  2. Renders AngularDart components via SSR for all other routes (including SPA navigation paths like /about, /products/42)
  3. Handles errors gracefully with a fallback error page

Note: the exact static path depends on your build configuration. The compiled client bundle is written under .dart_tool/build/generated/<package>/web/ (or build/web if you use build_runner build -o build:web). Adjust the static_file_handler directory accordingly.


Advanced: Per-Component Render Mode Configuration

You can fine-tune which components are rendered on the server and which are client-only. This is useful when you have:

  • SEO-critical pages (home, about, blog) that should always be SSR'd
  • User-specific dashboards that contain private data and shouldn't be SSR'd
  • Heavy interactive widgets that don't benefit from server rendering

Example: E-commerce Site with Mixed Rendering

// SEO-critical rendered on server for search engines
@Component(
  selector: 'product-listing',
  template: '<h1>Products</h1>'
      '<div *ngFor="let product of products">'
      '<a href="/products/{{product.id}}">{{product.name}}</a>'
      '<p>{{product.description}}</p></div>',
  renderMode: RenderMode.server,
)
class ProductListing {}

// Client-only user-specific shopping cart
@Component(
  selector: 'shopping-cart',
  template: '<h2>Your Cart</h2>'
      '<ul><li *ngFor="let item of items">'
      '{{item.name}} x{{item.quantity}} - ${{item.price}}</li></ul>',
  renderMode: RenderMode.client,
)
class ShoppingCart {}

// Automatic works in both contexts
@Component(
  selector: 'site-header',
  template: '<nav>'
      '<a href="/">Home</a>'
      '<a href="/products">Products</a>'
      '<a href="/cart">Cart ({{cartCount}})</a></nav>',
)
class SiteHeader {}

// Root component orchestrating everything
@Component(
  selector: 'app-root',
  template: '<site-header></site-header>'
      '<router-outlet [routes]="routes"></router-outlet>',
  directives: [SiteHeader, ProductListing, ShoppingCart],
)
class AppComponent {}

When a user visits /products:

  1. Server renders <product-listing> with full HTML (SEO-friendly!)
  2. Server outputs <shopping-cart></shopping-cart> as an empty placeholder
  3. Server renders <site-header> with navigation links
  4. Client hydrates the header and product listing from existing DOM
  5. Client renders the shopping cart fresh (since it wasn't in server HTML)

Prerendering vs Real-Time SSR

AngularDart offers two approaches for generating SEO-friendly HTML: SSR and Prerendering. Understanding the difference helps you choose the right one.

Aspect SSR (Real-Time) Prerendering (Static)
When HTML is generated On each request (or cache per route) At build time, before deployment
Server required? Yes needs a running Dart server No static files only
Dynamic content Can include real-time data Static snapshot at build time
Performance Slightly slower (server rendering overhead) Instant (served from CDN/static host)
Best for Content that changes frequently, personalized pages Marketing sites, blogs, documentation
Deployment complexity Needs a server process Just upload static files to any host

When to Use SSR

  • Your content changes frequently (e.g., news site, live dashboard)
  • You need real-time data in your HTML
  • You have many routes and don't want long build times
  • You're building an application with both public and authenticated pages

When to Use Prerendering

  • Your content is mostly static (marketing sites, documentation)
  • You want the simplest possible deployment (static hosting)
  • You need maximum performance (CDN caching)
  • You have a known set of routes that don't change often

You can also combine both: prerender your public pages for speed, and use SSR for dynamic routes behind authentication. See the Prerendering Guide for more details.


Common Patterns and Best Practices

1. Use RenderMode.automatic as Your Default

Unless you have a specific reason to force server or client rendering, use the default automatic mode:

@Component(
  selector: 'my-component',
  template: '<p>Hello</p>',
  // renderMode defaults to RenderMode.automatic no need to specify!
)
class MyComponent {}

This gives you SSR benefits when running on the server, and falls back gracefully to CSR if needed.

2. Use RenderMode.client for Browser-Only Features

If your component accesses browser APIs (window, document, localStorage), use client mode:

@Component(
  selector: 'browser-info',
  template: '<p>Window width: {{width}}px</p>',
  renderMode: RenderMode.client, // Can't access window on server!
)
class BrowserInfoComponent implements OnInit {
  int width = 0;

  @override
  void ngOnInit() {
    width = web.window.innerWidth;
  }
}

3. Use TransferState to Avoid Double API Calls

If you fetch data in ngOnInit() and your component is SSR'd, the same data will be fetched again on the client:

// Without TransferState data fetched twice!
@override
void ngOnInit() {
  _data = await _api.fetchData(); // Once on server, once on client
}

With TransferState:

// With TransferState data fetched only on server!
@override
void ngOnInit() async {
  final cached = TransferState.get<List>('my-data');
  if (cached != null) {
    _data = cached; // Use transferred state from server
  } else {
    _data = await _api.fetchData(); // Fetch on client if no SSR
    TransferState.set('my-data', _data); // Store for future use
  }
}

4. Handle Server-Side Routing Correctly

When using renderApplication(), always pass the requested URL so the router can match routes:

// Correct router knows which route to match
final html = await server.renderApplication(
  ng.appComponentFactory,
  url: request.uri.toString(), // e.g., "/products/42"
);

// Incorrect router defaults to "/" for all requests
final html = await server.renderComponent(ng.appComponentFactory);

5. Error Handling on the Server

Always wrap SSR rendering in try/catch blocks and provide fallback content:

httpServer.listen((request) async {
  try {
    final html = await server.renderApplication(
      ng.appComponentFactory,
      url: request.uri.toString(),
    );
    request.response
      ..statusCode = HttpStatus.ok
      ..write(html)
      ..close();
  } catch (e, st) {
    print('SSR error on ${request.uri.path}: $e');
    print(st);

    // Fallback: serve the SPA shell so client-side rendering can recover
    final indexHtml = await File('web/index.html').readAsString();
    request.response
      ..statusCode = HttpStatus.ok
      ..write(indexHtml)
      ..close();
  }
});

6. Separate Server and Client Entry Points

Keep your server entry point (web/main.server.dart) separate from your client entry point (web/main.dart). This allows:

  • The server to run on the Dart VM while the client runs compiled JavaScript
  • Clear separation of concerns
  • Easier debugging and maintenance

7. Use renderComponent() for Simple Cases, renderApplication() for Full HTML

  • renderComponent(): Returns only the component's HTML output. Use this when you want full control over the HTML wrapper (e.g., injecting custom <head> content).
  • renderApplication(): Returns a complete HTML document with <!DOCTYPE html>, <html>, <head>, and <body> tags. Use this for quick setup or when you don't need custom HTML structure.

8. Always Rebuild with --release and generate-stubs

After any change to your components or templates:

dart run build_runner build web --release
ngdart generate-stubs

The .template.dart files are generated by build_runner, then copied into the source tree by ngdart generate-stubs so dart bin/server.dart can resolve them on the VM.


Troubleshooting

Component Renders Empty on Server

Symptom: The server returns HTML but your component's content is missing (just the selector tag).

Causes and solutions:

  1. renderMode: RenderMode.client This component is intentionally not rendered on the server. Change to RenderMode.server or RenderMode.automatic.
  2. Missing directive/component in directives list Ensure all child components are listed in the parent's @Component(directives: [...]).
  3. Build not up to date Run dart run build_runner build web --release && ngdart generate-stubs again after making changes.

Hydration Not Working (Content Flickers)

Symptom: The page shows server-rendered content, then briefly flashes and re-renders from scratch.

Causes and solutions:

  1. Missing ng-server-context="ssr" detection Ensure your main.dart checks for this attribute before calling hydrateApplication().
  2. Server not adding the attribute Use renderApplication() instead of manually building HTML, or add ng-server-context="ssr" to your <html> tag yourself.
  3. Multiple AngularDart apps bootstrapped Ensure only one app is running per page.

TransferState Data Not Available on Client

Symptom: TransferState.get() returns null even though you set data on the server.

Causes and solutions:

  1. Calling before hydration completes hydrateApplication() calls TransferState.fromHtml() internally. Make sure you read TransferState after hydrateApplication() has finished (use await).
  2. Data not serializable Ensure your data can be serialized to JSON (no functions, circular references).
  3. Key mismatch The key used in set() and get() must match exactly (including the prefix).

Server Returns 500 Error

Symptom: All requests return a 500 Internal Server Error.

Causes and solutions:

  1. Check server logs The error message is printed to stdout. Look for stack traces.
  2. Port already in use Change the port: HttpServer.bind('localhost', 4001).
  3. Missing template files Ensure you've run dart run build_runner build web --release and ngdart generate-stubs, and that the .template.dart files exist in web/ and lib/.

Build Runner Fails with "URI has not been generated"

Symptom: Compilation fails with errors about main.template.dart not being found.

Solution: The .template.dart files are generated by the AngularDart compiler and don't exist in source control. Run build_runner first:

dart run build_runner build web --release
ngdart generate-stubs

If you still get analyzer warnings before the first build, the generated // ignore: uri_has_not_been_generated comments suppress them by design.

SSR Works Locally but Not on Production Server

Symptom: Everything works with dart bin/server.dart locally, but fails after deployment.

Causes and solutions:

  1. Missing build output Ensure you run dart run build_runner build web --release && ngdart generate-stubs as part of your deployment pipeline (not just locally).
  2. Wrong working directory The server uses relative paths (../web/main.server.dart). Run dart bin/server.dart from the project root.
  3. Memory limits SSR can be memory-intensive for large pages. Consider increasing your server's memory allocation or implementing response caching.

Next Steps

  • Prerendering Learn about static prerendering as an alternative to real-time SSR
  • SEO Guide Optimize meta tags, Open Graph, and structured data including server-aware SeoService with TransferState integration
  • Deployment Guide Deploy your AngularDart application with SSR to production

See Also

  • Angular Universal The Angular (TypeScript) equivalent of AngularDart's SSR implementation. AngularDart's API is inspired by Angular Universal.
  • Hydration in Angular 17+ Angular's client-side hydration feature, which inspired AngularDart's approach.