.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:
├── example.c3pkg.json
├── src
│ └── Windturbine.c3typ
└── ui
└── content
├── file1.html
└── scripts
└── file2.jsWhere Windturbine.c3typ exposes a restful endpoint by declaring @restful(endpoint='windturbines'). Once the application is deployed:
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:
├── example.c3pkg.json
├── src
│ └── Windturbine.c3typ
└── ui
└── content
├── index.html
├── about.html
├── contact
│ └── index.html
├── assets
│ ├── app.js
│ └── styles.css
└── missing-index-dir
└── page.htmlRequest behavior:
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:
// 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:
├── 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.pngConfiguration (config/UiService.Config/DEFAULT.json):
{
"name": "DEFAULT",
"spaMode": true
}Request behavior:
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:
// 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:
// Webpack example - set publicPath dynamically
__webpack_public_path__ = getBasePath() + "/";Alternative: Hash routing (if dynamic paths are not feasible)
// 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:
├── 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.pngConfiguration (config/UiService.Config/my-app.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:
{
"name": "my-app",
"spaMode": true,
"contentFolder": "custom-folder"
}This would serve content from ui/content/custom-folder/ instead.
Request behavior:
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 rootindex.htmlto enable client-side routing. - Named Services: Multiple applications can be served from different paths using named
UiServiceinstances. - Content Folder (
contentFolder): Serve content from a specific subdirectory ofui/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 whencontentFolderis configured
Root-First Lookup Behavior:
When contentFolder is configured, UiService uses root-first lookup:
- First checks
ui/content/{path}andui/content/pub/{path} - Then checks
ui/content/{contentFolder}/{path} - 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
c3AppUrlPrefixandc3UiServiceNamecookies to determine the correct base path for assets - Configure bundlers with dynamic
publicPathor 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:
├── 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.cssConfiguration (config/UiService.Config/console.json):
{
"name": "console",
"spaMode": true,
"contentFolder": "my-console"
}Request behavior:
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:
- When a request comes in for
/console/...,UiServicechecks if there's a named config called"console" - Since
console.jsonexists, the request is routed to the"console"UiServiceinstance - The
"console"instance serves content fromui/content/my-console/(as specified bycontentFolder) - Other paths like
/testerare unaffected and continue to use the DEFAULT namespace
Important notes:
- The named config's
namefield must match the URL path segment you want to override (e.g.,"console"for/console/...) - The
contentFolderspecifies where your override content lives (can be any folder name) - Other platform assets remain accessible unless you explicitly override them
- The
c3UiServiceNamecookie will be set to the namespace name (e.g.,"console")