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:
-
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
-
Add the lib-static dependency:
build.gradledependencies { 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
/assetsroot -
index.htmlresolution for directory-like URLs -
The minimum possible controller wiring
-
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.cssbody { font-family: sans-serif; margin: 2rem; } h1 { color: tomato; } -
Add the webapp controller:
src/main/resources/webapp/webapp.tsimport {requestHandler} from '/lib/enonic/static'; export const GET = requestHandler;That’s the whole thing —
requestHandleris exported directly as thegethandler. lib-router is not required here: the controller has no dynamic responses to split off from the static ones. -
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.htmlon 404 (so client-side routing works) -
Custom static folder location (
/webapp/files) -
Pairing with lib-router to keep the dispatch readable
-
Create a new React app using Vite
npm create vite@latest my-app-name -- --template react-ts cd my-app-name
-
Replace the default App.tsx file with this:
/src/vite/App.tsximport './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 AppYou 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. -
From the React app folder, build the static webapp:
npm install npm run build
-
Copy the static webapp files from the React app
dist/folder into your Enonic app. In this casesrc/main/resources/webapp/files/ -
In the Enonic app, create the webapp controller that will serve the SPA:
src/main/resources/webapp/webapp.tsimport { 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 cacheControlis a function — you can returnRESPONSE_CACHE_CONTROL.SAFEfor files without a content hash (likevite.svg), anddefaultCacheControl(immutable) for everything else.2 spaNotFoundHandlerservesindex.htmlon 404 so client-side routing keeps working. -
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
-
Create a controller that will serve the files:
site/controllers/myController/myController.tsimport { 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.yamlbelow2 Defines a custom location where the controller will look for static files -
Define the mapping in your
site.yamlfile.site/site.yamlkind: "Site" mappings: - controller: "/site/controllers/myController/myController.js" order: 1 pattern: "/mymapping(/.*)?" -
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.cssbody { color: green; } -
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
-
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.yamlkind: "AdminExtension" title: text: "Static asset demo" allow: - "role:system.admin" -
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.tsimport {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/intoreq.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.contextPathso they keep working under any admin mount. -
Place a stylesheet under
src/main/resources/static/my-extension/— the static-asset folder the controller’srootoption points at:src/main/resources/static/my-extension/styles.cssh1 { color: tomato; font-family: sans-serif; } -
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
GETtorequestHandler -
No lib-router, no template rendering
-
Define the ID provider descriptor:
src/main/resources/idprovider/idprovider.yamlkind: "IdProvider" mode: "MIXED" -
Add the controller — three lines, just delegate
GETto lib-static with a custom root:src/main/resources/idprovider/idprovider.tsimport {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. -
Place the login page and its stylesheet under
src/main/resources/static/idprovider/— the static-asset folder the controller’srootoption 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.cssform { display: grid; gap: 0.5rem; max-width: 18rem; font-family: sans-serif; } -
Build, deploy, and configure a site to use the ID provider — the login page is served straight from
idprovider/files/index.html.