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. Render Modes
  9. Hybrid Rendering in Detail
  10. TransferState: Sharing Data Between Server and Client
  11. Building a Custom SSR Server
  12. Advanced: Per-Component Render Mode Configuration
  13. Prerendering vs Real-Time SSR
  14. Common Patterns and Best Practices
  15. 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 no 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(
  AppComponentNgFactory,
  url: '/about',
);
// 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:

dart run angulardart_cli/bin/ngdart.dart new my_ssr_app --ssr
cd my_ssr_app
dart pub get

This creates a complete project structure with all the necessary files for SSR. The --ssr flag automatically:

  • Adds angulardart_server to your dependencies
  • Creates a server entry point (web/main.server.dart)
  • Creates an HTTP server (bin/server.dart)
  • Modifies main.dart to detect and handle hydration
  • Configures build_runner for dual compilation (client + server)

Building the Server Bundle

After creating your project, you need to compile it for the server:

dart run build_runner build web/main.server.dart

This compiles your AngularDart application using Dart Dev Compiler (DDC), which produces JavaScript that can run on the Dart VM. The output is written to .dart_tool/build/entrypoint/.

Running the Server

dart bin/server.dart

Then open http://localhost:4000 in your browser. You'll see your AngularDart application rendered server-side with full hydration on the client.


Project Structure

A project created with --ssr has this structure:

my_ssr_app/
├── bin/
│   └── server.dart              # HTTP server using shelf + angulardart_server
├── lib/
│   ├── main.dart                # Client entry point (with hydration detection)
│   └── main.template.dart       # Generated by build_runner
├── web/
│   ├── index.html               # HTML shell (served for non-SSR requests)
│   ├── main.server.dart         # Server entry point (compiled separately)
│   └── main.server.template.dart # Generated server bundle
├── pubspec.yaml                 # Dependencies include angulardart_server
└── build_runner.yaml

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 for the server to use
lib/main.dart Client entry point detects if page was SSR'd and hydrates, or runs normally
pubspec.yaml Contains angulardart_server >=1.0.0 <2.0.0 dependency

Understanding Each File

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

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.HelloWorldComponentNgFactory;

This file is compiled separately from your client code using build_runner build web/main.server.dart. It produces a Dart VM-compatible bundle that the server uses to render components. The // ignore: uri_has_not_been_generated comment suppresses a warning because main.template.dart doesn't exist until after compilation.

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.template.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 {
      try {
        final html = await server.renderApplication(
          ng.HelloWorldComponentNgFactory,
          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. The response is sent with Content-Type: text/html

The Client Entry Point (lib/main.dart)

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

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

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

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. No conditional compilation needed just runtime detection.


Render Modes

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

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

  /// Client-side only (standard SPA behavior)
  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 =
      web.window.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() 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 (like functions or custom Dart objects without a JSON representation) are serialized as strings using their toString() output.


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.template.dart' as ng;

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

  // Create a pipeline that serves both static files and SSR content
  var pipeline = shelf.Pipeline();

  // Serve static assets (JS, CSS, images) from the build/web directory
  pipeline = pipeline.addMiddleware(
    static_file_handler.createFileSystemHandler(
      '../build/web',
      defaultDocument: 'index.html',
    ),
  );

  // For Angular routes, serve SSR content
  pipeline = pipeline.addMiddleware((shelf.Request request) async {
    final path = request.url.path;

    // Let static files pass through (JS, CSS, images)
    if (_isStaticAsset(path)) {
      return null; // Pass to next middleware
    }

    // SSR for all other routes
    try {
      final html = await server.renderApplication(
        ng.AppComponentNgFactory,
        url: request.url.toString(),
      );

      return shelf.Response(200, body: html, headers: {
        'Content-Type': 'text/html; charset=utf-8',
      });
    } catch (e) {
      return shelf.Response(500, body: '<h1>Server Error</h1>');
    }
  });

  final handler = shelf.createStaticHandler('../build/web');
  final combinedHandler = pipeline.addHandler(handler);

  await HttpServer.bind('localhost', 4000).then((httpServer) {
    httpServer.listen((request) async {
      final response = await combinedHandler(request);
      request.response.addStream(response.read());
      await request.response.flush();
      await request.response.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

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(
  AppComponentNgFactory,
  url: request.url.toString(), // e.g., "/products/42"
);

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

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(
      AppComponentNgFactory,
      url: request.url.toString(),
    );
    request.response
      ..statusCode = HttpStatus.ok
      ..write(html)
      ..close();
  } catch (e, st) {
    print('SSR error on ${request.url.path}: $e');
    print(st);

    // Fallback: serve the SPA shell so client-side rendering can recover
    final indexHtml = await File('build/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 (lib/main.dart). This allows:

  • Different compilation targets (DDC for server, dart2js for client)
  • 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.

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/main.server.dart 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 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 build_runner build web/main.server.dart and the output exists in .dart_tool/build/entrypoint/.

Build Runner Fails with "URI has not been generated"

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

Solution: You need to run build_runner first:

dart run build_runner build web/main.server.dart

The .template.dart files are generated by the AngularDart compiler and don't exist in source control.

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 build_runner build web/main.server.dart as part of your deployment pipeline (not just locally).
  2. Wrong working directory The server uses relative paths (../web/main.template.dart). Adjust for your deployment structure.
  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 for search engines
  • 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.