C3 AI Documentation Home

.Topic Type: Reference Audience: Customer Category: UI Title: Serving static content with UI Service Abstract: Learn how UI Service serves static content and how to configure it to serve single page applications

UiService is the default UI content server and router in the C3 platform. It handles requests for web resources that are not managed by custom endpoints (such as types that mix in Restful or use @restful).

Packages can define their own named UiService by seeding a UiService.Config with the desired name. The default configuration provides static web server behavior, where all requests must match a file in ui/content or ui/content/pub, and directory paths are redirected to their index.html if it exists.

The default UiService instance name is "DEFAULT". Instances with this name handle requests at the root path /.

Default serving behavior

To better understand the default routing behavior in C3 AI Platform, let's look at a typical application:

Text
├── example.c3pkg.json
├── src
│   └── Windturbine.c3typ
└── ui
    └── content
        ├── file1.html
        └── scripts
            └── file2.js

Where Windturbine.c3typ exposes a restful endpoint by declaring @restful(endpoint='windturbines'). Once the application is deployed:

Text
curl $CLUSTER/$ENV/$APP/windturbines
# Matches the restful endpoint, so is handled by that endpoint. Returns HTTP 200 with a list of wind turbines

curl $CLUSTER/$ENV/$APP/file1.html
# Does not match any restful endpoint, so is handled by UI service. Returns HTTP 200 with content of file1.html

curl $CLUSTER/$ENV/$APP/scripts/file2.js
# Does not match any restful endpoint, so is handled by UI service. Returns HTTP 200 with content of scripts/file2.js`

curl $CLUSTER/$ENV/$APP/solar
# Does not match any restful endpoint nor any file handled by UI Service. Returns HTTP 404.

Example Use Cases

The behavior of UiService depends on its configuration, as defined in UiService.Config. Below are comprehensive examples showing different scenarios with actual folder structures and request behaviors.

1. Static Web Server (default, spaMode disabled)

This is the default behavior where UiService acts as a traditional static web server. Missing files return 404, and directory requests redirect to their index files.

Package structure:

Text
├── example.c3pkg.json
├── src
│   └── Windturbine.c3typ
└── ui
    └── content
        ├── index.html
        ├── about.html
        ├── contact
        │   └── index.html
        ├── assets
        │   ├── app.js
        │   └── styles.css
        └── missing-index-dir
            └── page.html

Request behavior:

Text
curl $CLUSTER/$ENV/$APP/
# Returns HTTP 200 with content of ui/content/index.html

curl $CLUSTER/$ENV/$APP/about.html
# Returns HTTP 200 with content of ui/content/about.html

curl $CLUSTER/$ENV/$APP/contact
# Returns HTTP 302 redirect to $CLUSTER/$ENV/$APP/contact/index.html

curl $CLUSTER/$ENV/$APP/contact/index.html
# Returns HTTP 200 with content of ui/content/contact/index.html

curl $CLUSTER/$ENV/$APP/assets/app.js
# Returns HTTP 200 with content of ui/content/assets/app.js

curl $CLUSTER/$ENV/$APP/missing-index-dir
# Returns HTTP 404 (directory has no index.html)

curl $CLUSTER/$ENV/$APP/nonexistent.html
# Returns HTTP 404 (file does not exist)

Client-side routing:

Since missing routes return 404, Single Page Applications (without configuring spaMode) must use hash-based routing to avoid server conflicts:

JavaScript
// Required: Use hash routing since server returns 404 for missing paths
// Routes: /#/dashboard, /#/users/123
const router = new HashRouter();

2. Single Page Application (SPA) Mode (spaMode enabled)

In SPA mode, missing files return the root index.html to enable client-side routing. This allows frontend frameworks like React, Vue, or Angular to handle routing internally.

Package structure:

Text
├── example.c3pkg.json
├── config
│   └── UiService.Config
│       └── DEFAULT.json
├── src
│   └── Windturbine.c3typ
└── ui
    └── content
        ├── index.html          # SPA entry point
        ├── static
        │   ├── js
        │   │   └── app.bundle.js
        │   └── css
        │       └── main.css
        └── assets
            └── logo.png

Configuration (config/UiService.Config/DEFAULT.json):

JSON
{
  "name": "DEFAULT",
  "spaMode": true
}

Request behavior:

Text
curl $CLUSTER/$ENV/$APP/
# Returns HTTP 200 with content of ui/content/index.html

curl $CLUSTER/$ENV/$APP/static/js/app.bundle.js
# Returns HTTP 200 with content of ui/content/static/js/app.bundle.js

curl $CLUSTER/$ENV/$APP/dashboard
# Returns HTTP 200 with content of ui/content/index.html (SPA fallback)

curl $CLUSTER/$ENV/$APP/users/123
# Returns HTTP 200 with content of ui/content/index.html (SPA fallback)

curl $CLUSTER/$ENV/$APP/api/data
# Returns HTTP 200 with content of ui/content/index.html (SPA fallback)

Client-side routing considerations:

With spaMode enabled, missing routes return the SPA's index.html instead of 404, enabling normal browser routing. However, proper asset path resolution is critical for all referenced resources.

Router configuration:

JavaScript
// Normal browser routing works because server fallbacks to index.html
function getBasePath() {
  const appUrlPrefix = getCookie("c3AppUrlPrefix") || "";
  const uiServiceName = getCookie("c3UiServiceName");
  return uiServiceName === "DEFAULT"
    ? `/${appUrlPrefix}`
    : `/${appUrlPrefix}/${uiServiceName}`;
}

const router = new BrowserRouter({ basename: getBasePath() });

Asset path configuration:

All assets that reference other assets (JS, CSS, images) must use the correct public path. Configure your bundler accordingly:

JavaScript
// Webpack example - set publicPath dynamically
__webpack_public_path__ = getBasePath() + "/";

Alternative: Hash routing (if dynamic paths are not feasible)

JavaScript
// Simpler option if you can't configure dynamic public paths
const router = new HashRouter();

3. Named UiService for SPA

When using a named UiService, you can serve multiple applications from different paths. This example shows a named service called "my-app".

Package structure:

Text
├── example.c3pkg.json
├── config
│   └── UiService.Config
│       └── my-app.json
├── src
│   └── Windturbine.c3typ
└── ui
    └── content
        └── my-app              # Named UiService content (default: same as service name)
            ├── index.html      # SPA entry point for my-app
            ├── static
            │   ├── js
            │   │   └── app.bundle.js
            │   └── css
            │       └── main.css
            └── assets
                └── logo.png

Configuration (config/UiService.Config/my-app.json):

JSON
{
  "name": "my-app",
  "spaMode": true
}

By default, a named service serves content from ui/content/{service-name}/. You can override this with the contentFolder option:

JSON
{
  "name": "my-app",
  "spaMode": true,
  "contentFolder": "custom-folder"
}

This would serve content from ui/content/custom-folder/ instead.

Request behavior:

Text
curl $CLUSTER/$ENV/$APP/
# Returns HTTP 200 with content of ui/content/index.html if available (DEFAULT UiService)

curl $CLUSTER/$ENV/$APP/my-app/
# Returns HTTP 200 with content of ui/content/my-app/index.html

curl $CLUSTER/$ENV/$APP/my-app/static/js/app.bundle.js
# Returns HTTP 200 with content of ui/content/my-app/static/js/app.bundle.js

curl $CLUSTER/$ENV/$APP/my-app/dashboard
# Returns HTTP 200 with content of ui/content/my-app/index.html (SPA fallback)

curl $CLUSTER/$ENV/$APP/my-app/users/123
# Returns HTTP 200 with content of ui/content/my-app/index.html (SPA fallback)

Client-side routing:

Named services use the same routing patterns as described in the SPA section above. The cookies will reflect the service name (e.g., c3UiServiceName="my-app").

Summary

Basic Configurations:

  • Static Web Server: Default behavior where missing files return 404 and directories redirect to their index files.
  • SPA Mode (spaMode): Missing files return the root index.html to enable client-side routing.
  • Named Services: Multiple applications can be served from different paths using named UiService instances.
  • Content Folder (contentFolder): Serve content from a specific subdirectory of ui/content/.

Key directories:

  • ui/content/ - Default asset location (always checked first for root-first lookup)
  • ui/content/pub/ - Public assets (also checked during root-first lookup)
  • ui/content/[contentFolder]/ - Custom content folder when contentFolder is configured

Root-First Lookup Behavior:

When contentFolder is configured, UiService uses root-first lookup:

  1. First checks ui/content/{path} and ui/content/pub/{path}
  2. Then checks ui/content/{contentFolder}/{path}
  3. For SPA mode, falls back to ui/content/{contentFolder}/index.html

This ensures platform static assets (like /console and /tester) always work, even when a custom contentFolder is configured for the DEFAULT configuration.

Client-side routing for SPAs:

  • Static Web Server (default): Requires hash-based routing since missing routes return 404
  • SPA Mode (spaMode): Enables normal browser routing with proper asset path configuration
  • Use c3AppUrlPrefix and c3UiServiceName cookies to determine the correct base path for assets
  • Configure bundlers with dynamic publicPath or use hash routing as fallback

The default UiService instance name is "DEFAULT" and handles requests at the root path /.


Overriding Platform Static Assets

Platform packages provide static assets like /console and /tester that are served from ui/content/console/ and ui/content/pub/tester/ respectively. You can override these assets by creating a named UiService.Config with the same name as the path you want to override.

Example: Overriding the Console

To replace the platform's console with your own custom console:

Package structure:

Text
├── example.c3pkg.json
├── config
│   └── UiService.Config
│       └── console.json
└── ui
    └── content
        └── my-console           # Your custom console content
            ├── index.html       # Custom console entry point
            ├── app.js
            └── styles.css

Configuration (config/UiService.Config/console.json):

JSON
{
  "name": "console",
  "spaMode": true,
  "contentFolder": "my-console"
}

Request behavior:

Text
curl $CLUSTER/$ENV/$APP/console
# Returns HTTP 302 redirect to $CLUSTER/$ENV/$APP/console/index.html

curl $CLUSTER/$ENV/$APP/console/index.html
# Returns HTTP 200 with YOUR custom console (ui/content/my-console/index.html)
# NOT the platform's console

curl $CLUSTER/$ENV/$APP/console/dashboard
# Returns HTTP 200 with ui/content/my-console/index.html (SPA fallback)

curl $CLUSTER/$ENV/$APP/tester
# Still returns the platform's tester (no override configured)

How it works:

  1. When a request comes in for /console/..., UiService checks if there's a named config called "console"
  2. Since console.json exists, the request is routed to the "console" UiService instance
  3. The "console" instance serves content from ui/content/my-console/ (as specified by contentFolder)
  4. Other paths like /tester are unaffected and continue to use the DEFAULT namespace

Important notes:

  • The named config's name field must match the URL path segment you want to override (e.g., "console" for /console/...)
  • The contentFolder specifies where your override content lives (can be any folder name)
  • Other platform assets remain accessible unless you explicitly override them
  • The c3UiServiceName cookie will be set to the namespace name (e.g., "console")

Was this page helpful?