arrow-down
  1. Intro
  2. Usage
  3. API

Usage and examples

Contents

Introduction

There are multiple ways of deploying and using lib-static in your application. Below we explore the most common approaches. This includes how you may set up lib-static to serve different files, using different cache headers.

Common

Follow these initial steps to get started, they are common for all the examples below:

  1. Use an existing Enonic app, or create a new one with the following command:

    enonic sandbox create static_tutorial -t essentials -f
    enonic create com.example.static -r starter-ts
  2. Add the lib-static dependency:

    build.gradle
    dependencies {
      include "com.enonic.lib:lib-static:${libVersion}"
      include "com.enonic.lib:lib-router:${routerVersion}" (1)
    }
    1 Optional. lib-router is not required to use lib-static, but it is very handy for splitting static resources and dynamic ones inside the same controller, as demonstrated in the examples below. Skip it if your controller only serves static assets.
    Replace ${libVersion} / ${routerVersion} with the desired versions of lib-static and lib-router.

You can now move on to the various examples below:

Webapp example

The simplest way to serve a static site with lib-static: a webapp controller that delegates every GET to requestHandler with all defaults — root /assets, index.html as the directory index, immutable cache headers.

Read more about XP webapps.

This example demonstrates

  • Serving files from the default /assets root

  • index.html resolution for directory-like URLs

  • The minimum possible controller wiring

  1. Place files under src/main/resources/assets/:

    src/main/resources/assets/index.html
    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <title>lib-static — minimal webapp</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <h1>Hello from lib-static</h1>
        <p>This page is served straight from <code>/assets/</code> by <code>requestHandler</code>.</p>
    </body>
    </html>
    src/main/resources/assets/style.css
    body {
        font-family: sans-serif;
        margin: 2rem;
    }
    
    h1 {
        color: tomato;
    }
  2. Add the webapp controller:

    src/main/resources/webapp/webapp.ts
    import {requestHandler} from '/lib/enonic/static';
    
    export const GET = requestHandler;

    That’s the whole thing — requestHandler is exported directly as the get handler. lib-router is not required here: the controller has no dynamic responses to split off from the static ones.

  3. Build, deploy, and visit http://localhost:8080/webapp/com.example.static.

Webapp SPA example

For a more involved case — a React (or any other framework) single-page app with build-time fingerprinted assets, custom cache rules, and a notFound fallback to index.html — lib-static and lib-router pair to handle both static asset serving and the SPA fallback in one controller.

This example demonstrates advanced use-cases like:

  • Custom Cache control handling per file

  • Fallback to SPA index.html on 404 (so client-side routing works)

  • Custom static folder location (/webapp/files)

  • Pairing with lib-router to keep the dispatch readable

  1. Create a new React app using Vite

    npm create vite@latest my-app-name -- --template react-ts
    cd my-app-name
  2. Replace the default App.tsx file with this:

    /src/vite/App.tsx
    import './App.css'
    import { BrowserRouter, Link, Route, Routes } from "react-router-dom";
    
    function App() {
      return (
          <BrowserRouter basename='/webapp/com.example.static'> {/* <1> */}
            <Routes>
              <Route path="/" element={<Link to="/bookmarkable">Bookmarkable</Link>}/>
              <Route path="/bookmarkable" element={<Link to="/">Home</Link>}/>
              <Route path="*" element={<div>
                <h1>404</h1>
                <Link to="/">Go home</Link>
              </div>}/>
            </Routes>
          </BrowserRouter>
      )
    }
    
    export default App
    You may serve the webapp from any context path (and domain) using vHosts. Make sure the baseUrl in your React app is compliant with the deployment.
  3. From the React app folder, build the static webapp:

    npm install
    npm run build
  4. Copy the static webapp files from the React app dist/ folder into your Enonic app. In this case src/main/resources/webapp/files/

  5. In the Enonic app, create the webapp controller that will serve the SPA:

    src/main/resources/webapp/webapp.ts
    import {
      RESPONSE_CACHE_CONTROL,
      defaultCacheControl,
      requestHandler,
      spaNotFoundHandler,
    } from '/lib/enonic/static';
    import Router from '/lib/router';
    
    const router = Router();
    
    router.get('{path:.*}', (request) => requestHandler(request, {
      cacheControl: ({contentType, path, resource}) => { (1)
        // The "webapp/files/vite.svg" file has no contentHash
        if (path.startsWith('/vite.svg')) {
          return RESPONSE_CACHE_CONTROL.SAFE;
        }
        // Fall back to defaultCacheControl for all other assets.
        return defaultCacheControl({contentType, path, resource});
      },
      notFound: spaNotFoundHandler, (2)
      root: '/webapp/files',
    }));
    
    export const GET = (request) => router.dispatch(request);
    1 cacheControl is a function — you can return RESPONSE_CACHE_CONTROL.SAFE for files without a content hash (like vite.svg), and defaultCacheControl (immutable) for everything else.
    2 spaNotFoundHandler serves index.html on 404 so client-side routing keeps working.
  6. Build and deploy the Enonic app containing the SPA, you should see the result by visiting http://localhost:8080/webapp/com.example.static.

Site mapping example

Developers using site engine may want to serve the assets from a pretty URL, rather than a service endpoint.

This example demonstrates

  • use of relativeMappedPath

  • using a custom static folder location

  1. Create a controller that will serve the files:

    site/controllers/myController/myController.ts
    import {
      mappedRelativePath,
      requestHandler,
    } from '/lib/enonic/static';
    import Router from '/lib/router';
    
    const router = Router();
    router.get('{path:.*}', (request) => {
      log.info('mapped get request:%s', JSON.stringify(request, null, 4));
      return requestHandler(
        request,
        {
          relativePath: mappedRelativePath('mymapping'), (1)
          root: '/site/controllers/myController/files', (2)
        }
      );
    });
    
    export const all = (request) => router.dispatch(request);
    1 relativeMappedPath must match the mapping in site.yaml below
    2 Defines a custom location where the controller will look for static files
  2. Define the mapping in your site.yaml file.

    site/site.yaml
    kind: "Site"
    mappings:
      - controller: "/site/controllers/myController/myController.js"
        order: 1
        pattern: "/mymapping(/.*)?"
  3. Add a file to the static files folder. In this case we place the files together with the controller:

    site/controllers/myController/files/css/styles.css
    body {
        color: green;
    }
  4. Build, deploy and add the application to a site. The files will be served from site-relative mapping path (in this case /mymapping/css/styles.css).

AdminExtension example

In an admin extension, the controller handles everything that hits the extension URL — both the widget HTML and any assets it references. lib-static + lib-router pair cleanly for that: one route dispatches static requests, another renders the extension UI.

This example demonstrates

  • Serving static assets from inside an admin extension

  • lib-router dispatch with a static-prefix route

  • Building asset URLs relative to the extension’s contextPath

  1. Define the extension descriptor. The extension is mounted by XP at a URL like /_/admin:extension/com.example.static:my-extension.

    src/main/resources/admin/extensions/my-extension/my-extension.yaml
    kind: "AdminExtension"
    title:
      text: "Static asset demo"
    allow:
      - "role:system.admin"
  2. Add the controller. lib-router routes anything under /_static/ to lib-static; everything else renders the widget HTML.

    src/main/resources/admin/extensions/my-extension/my-extension.ts
    import {requestHandler} from '/lib/enonic/static';
    import Router from '/lib/router';
    
    const router = Router();
    
    router.get('/_static/{path:.*}', (request) => requestHandler(request, { (1)
      root: '/static/my-extension',
      relativePath: (req) => req.pathParams.path,
    }));
    
    router.get('{path:.*}', (request) => ({ (2)
      contentType: 'text/html',
      body: `<link rel="stylesheet" href="${request.contextPath}/_static/styles.css"/>`
        + `<h1>Static asset demo</h1>`,
    }));
    
    export const GET = (request) => router.dispatch(request);
    1 Requests with the /_static/ prefix are forwarded to lib-static. The router pattern captures everything after /_static/ into req.pathParams.path, which is handed straight to lib-static as the relative path.
    2 The catch-all route renders the widget. Asset URLs are built relative to request.contextPath so they keep working under any admin mount.
  3. Place a stylesheet under src/main/resources/static/my-extension/ — the static-asset folder the controller’s root option points at:

    src/main/resources/static/my-extension/styles.css
    h1 {
        color: tomato;
        font-family: sans-serif;
    }
  4. Build, deploy, and open the extension in the admin UI — the stylesheet loads via ${contextPath}/_static/styles.css.

IdProvider example

The login page of an ID provider is often plain static HTML — a form, a stylesheet, maybe a logo. The controller can serve it directly with requestHandler, the same way as the minimal webapp example. The form’s action="login" posts the submitted credentials to the ID provider’s login flow.

This example demonstrates

  • Serving the entire login page (HTML + CSS) as static files from inside an ID provider

  • A controller that does nothing but delegate GET to requestHandler

  • No lib-router, no template rendering

  1. Define the ID provider descriptor:

    src/main/resources/idprovider/idprovider.yaml
    kind: "IdProvider"
    mode: "MIXED"
  2. Add the controller — three lines, just delegate GET to lib-static with a custom root:

    src/main/resources/idprovider/idprovider.ts
    import {requestHandler} from '/lib/enonic/static';
    
    export const GET = (request) => requestHandler(request, {root: '/static/idprovider'});
    A real ID provider would also export login / logout / handle401 — those are out of scope for this example, which only shows the static-asset wiring.
  3. Place the login page and its stylesheet under src/main/resources/static/idprovider/ — the static-asset folder the controller’s root option points at:

    src/main/resources/static/idprovider/index.html
    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Sign in</title>
        <link rel="stylesheet" href="login.css">
    </head>
    <body>
        <form action="login" method="post">
            <input name="user" placeholder="user" required>
            <input name="password" type="password" placeholder="password" required>
            <button type="submit">Sign in</button>
        </form>
    </body>
    </html>
    src/main/resources/static/idprovider/login.css
    form {
        display: grid;
        gap: 0.5rem;
        max-width: 18rem;
        font-family: sans-serif;
    }
  4. Build, deploy, and configure a site to use the ID provider — the login page is served straight from idprovider/files/index.html.


Contents

Contents