Upgrading Enonic apps
Contents
This page is the entry point for upgrading an Enonic application. Choose the guidance that applies to the application’s current XP version and the version you are targeting.
When building a new Enonic application, use the TypeScript Starter (starter-ts). It provides a project structure and build setup for TypeScript development. |
Choosing upgrade notes
-
Coming from XP 7: Follow Upgrading Enonic apps from XP 7 to XP 8. That guide covers the changes needed to reach an XP 8.0 baseline.
-
Already on XP 8: Read the release sections between your current and target versions, in version order. Apply required changes and any guidance for features your application uses. Optional deprecation clean-ups can be done separately.
-
Moving from XP 7 to a later XP 8 release: Combine the XP 7 guide with the subsequent release notes up to your target version. You can apply the changes together and build directly for the target version; an intermediate deployment on XP 8.0 is not required.
The same selection applies when using an AI coding agent: start here, then read the linked guide and release sections that apply to the upgrade.
Version bump
Set the platform version in gradle.properties to the release you are targeting:
xpVersion = 8.1.0
TypeScript projects also need the matching type definitions, or the parameters and functions added in the release will not type-check:
{
"devDependencies": {
"@enonic-types/global": "^8.1.0",
"@enonic-types/lib-content": "^8.1.0"
}
}
Then rebuild and deploy:
enonic project deploy
The app builds and runs. What follows is work that can be done at your own pace.
XP 8.0
XP 8.0 is the baseline for this documentation. The XP 7 migration guide covers changes to the build system, descriptors, application code and APIs when moving from XP 7.
XP 8.1
No breaking changes for JavaScript application code. Every API deprecated in 8.1 still works, and every replacement ships with 8.1.0, apart from the Java macro instruction noted below.
Deprecations
lib-content and lib-node
getChildren() in lib-content and findChildren() in lib-node are deprecated. A query with parent does the same job: it takes the same path or id, and where sort is omitted it applies the child order stored on the parent, so the result comes back in the order it did before.
import {getChildren} from '/lib/xp/content';
const result = getChildren({
key: '/mysite/articles',
start: 0,
count: 20
});
import {query} from '/lib/xp/content';
const result = query({
parent: '/mysite/articles',
start: 0,
count: 20
});
hits carries the same contents as before, and total the same count.
const result = repo.findChildren({
parentKey: '/content',
count: 100,
recursive: true
});
const result = repo.query({
parent: '/content',
count: 100,
recursive: true
});
Three parameters change name or shape:
-
childOrder -
Use
sort, which takes the same expression. Omit it to keep the parent’s child order. -
countOnly -
Ask for no hits instead -
count: 0- and readtotal. -
recursive -
Same name, same meaning. A subtree has no child order of its own, so pass
sortwhere the order of a recursive result matters.
Moving to a query also opens up everything a query can do that getChildren() could not: filters, content types, aggregations, highlighting, and the returns parameter that has each hit carry named index fields instead of the whole content.
lib-task
executeFunction() is deprecated and will be removed together with the Nashorn engine. It is also unsupported on the GraalJS, where a function cannot cross from one script context to another - so this is the one clean-up to do before trying that engine.
Move the body of the function into a named task, and submit it by descriptor.
import {executeFunction} from '/lib/xp/task';
const taskId = executeFunction({
description: 'Reindex the catalog',
func: () => {
reindexCatalog(100);
}
});
kind: "Task"
description: "Reindex the catalog"
form:
- type: "Long"
name: "batchSize"
label: "Items per batch"
occurrences:
min: 1
max: 1
export function run(params: { batchSize: number }): void {
reindexCatalog(params.batchSize);
}
import {submitTask} from '/lib/xp/task';
const taskId = submitTask({
descriptor: 'reindex-catalog',
config: {
batchSize: 100
}
});
Two differences to plan for. A named task takes its input as config, validated against the descriptor’s form, rather than closing over the variables around it - anything the function read from its surroundings has to become a parameter. And a named task is distributable: the cluster picks a node that accepts it, which may not be the node that submitted it, so the task must not depend on running anywhere in particular.
lib-portal
The params parameter is deprecated on imageUrl() and attachmentUrl(). The image API reads no query parameters but quality, background and filter, and the attachment API none but download, all set by their own parameters. Any other custom parameter only makes URLs to the same media differ, and one of those names - such as params: {filter: 'blur(10)'} - changes the image past the parameter meant for it. Use quality, background, filter and download instead.
The baseUrl parameter is deprecated on imageUrl(), attachmentUrl() and apiUrl(). Where media or an API is served from is a deployment decision, and passing it per call means every call site has to agree on it.
import {imageUrl} from '/lib/xp/portal';
const url = imageUrl({
id: content._id,
scale: 'width(800)',
baseUrl: 'https://cdn.example.com'
});
import {imageUrl} from '/lib/xp/portal';
const url = imageUrl({
id: content._id,
scale: 'width(800)'
});
For media, declare the host once in com.enonic.xp.portal.cfg. A mount that serves the media APIs itself always wins, so a site needs both keys:
media.defaultBaseUrl = https://cdn.example.com
legacy.mediaApiAutoMount.enabled = false
A Base URL configured on the site itself does the same for that one site. See serving media from another host.
For APIs, the virtual host that receives the request declares where they live:
mapping.example.context.portal.apiBaseUrl = https://apis.example.com
See API locations.
Both are read at URL generation, so the same code produces the right URL in every environment - and in a preview, a task or a headless client, where there may be no request to anchor to.
Setting a project or branch on a request-following URL function switched it from the request to configuration, even for the request’s own project, so the URL left the virtual host the request came through. These parameters are deprecated in favour of the URL parts functions, which resolve URLs from configuration alone:
-
projectandbranchonpageUrl(): usepageUrlParts()with ascopefromportalScope(). -
id,path,projectandbranchonbaseUrl(): useportalScope(), withkeynaming the site.baseUrl()keepstype, and without parameters it is the base URL of the current site request.
import {pageUrl} from '/lib/xp/portal';
const url = pageUrl({
path: '/my-site/posts/first-post',
project: 'myproject',
branch: 'master',
type: 'absolute'
});
import {pageUrlParts, portalScope} from '/lib/xp/portal';
const scope = portalScope({key: '/my-site', project: 'myproject', branch: 'master'});
const parts = pageUrlParts({path: '/my-site/posts/first-post', scope});
const url = (parts.baseUrl ?? 'https://www.example.com') + parts.path + parts.queryString;
lib-export
batchSize on exportNodes() is ignored. An export now reads each node at the exact version its scan observed, one at a time, so a subtree that changes while the export runs is written as it was seen rather than as a mixture of old and new. There is no read batch left to size - drop the parameter.
lib-app, lib-schema and lib-io
modifiedTime is deprecated on the icons, content types, schemas and application descriptors returned by lib-content, lib-schema and lib-app, and so is Resource.getTimestamp() in lib-io. These read from application resources, where the value comes from a jar entry timestamp - and build tools normalize those to a constant so that builds are reproducible, Gradle by default since version 9. Two different builds of the same app therefore report the same value.
Code that used them as a cache key or an invalidation signal was therefore already unreliable:
const cacheKey = `icon-${contentType.icon.modifiedTime}`;
There is no drop-in replacement, and no general rule - what to key on depends on what the key is protecting. For an icon, or any other binary resource, a hash of the content itself is the dependable choice: it changes exactly when the bytes do, and only then. An application version is weaker than it looks - it stays the same across redeploys while you develop, even though each deploy is a new build.
Java APIs
NodeService
NodeService gains two methods for reading a subtree: list streams every entry beneath a parent in path order, filtered by what the caller may read, and enumerate returns the same entries one bounded batch at a time, with a cursor. ListNodesResult and its recursive flag are removed - the listing always covers the whole subtree, so code that collected ids through ListNodesResult moves to list.
ApplicationListener
ApplicationListener is deprecated. It misses applications that were already active when the listener registered. Track Application services with a service tracker instead: addingService replays every active application on open and delivers future ones, and removedService is delivered before any replacement is registered.
MacroService
MacroService.postProcessInstructionSerialize() is deprecated. It turned a macro into an HTML comment that XP replaced with the macro’s output when rendering the page. XP no longer does that for these comments: the page shows the macro as written, such as [mymacro]. Pass rich text through processHtml() instead; the macros it finds are rendered with the page.
ApplicationInvalidator
ApplicationInvalidator and ApplicationInvalidationLevel are deprecated for the same reason as ApplicationListener: the callback arrives only after the application’s services are gone, possibly after a replacement has started. Track Application services instead.
Children and paths
ContentService.findByParent(), findIdsByParent() and findPaths() are deprecated, as are NodeService.findByParent() and NodeQuery.Builder.withPath(). A query does the same job and accepts every constraint a search supports:
-
ContentService.find()withContentQuery.Builder.parentPath()orparentId(), followed bygetByIds()for the contents actually needed; -
find()withreturnFields()naming_path, in place offindPaths(); -
NodeService.findByQuery()withNodeQuery.Builder.parent(), andreturnFields()withNodeIndexPath.PATHin place ofwithPath().
Progress listeners
Listener methods are renamed, so that every listener names its work the same way. The old methods are still called by the default implementations of the new ones:
-
resolved()replacesPushContentListener.contentResolved(),NodeExportListener.nodeResolved(),NodeImportListener.nodeResolved(), andsetTotal()ofApplyPermissionsListenerandApplyNodePermissionsListener; -
CleanUpAuditLogListener.recordsDeleted()replacesprocessed(); itsstart()andfinished()are deprecated without a replacement.
Export and import
ExportNodesParams.Builder.batchSize() is ignored, as in lib-export. ImportNodesParams.Builder.xsltFileName() is ignored: use xslt() with a VirtualFile.
Modified times
getModifiedTime() is deprecated on Icon, BaseSchema, MacroDescriptor, ComponentDescriptor, CmsDescriptor, SiteDescriptor and StyleDescriptor, and so is Resource.getTimestamp(), for the reasons given for modifiedTime in JavaScript.
Portal URLs
The Java URL parameters follow the JavaScript ones:
-
baseUrlonImageUrlParams,AttachmentUrlParams,ProcessHtmlParams,ImageUrlGeneratorParamsandAttachmentUrlGeneratorParams: configure where the media APIs are served, with a virtual host mapping ormedia.defaultBaseUrlincom.enonic.xp.portal.cfg; -
setBaseUrl()onApiUrlParamsandApiUrlGeneratorParams: expose the API location as a virtual host mapping; -
projectName()andbranch()onPageUrlParams: usePortalUrlService.pageUrlParts()with aPortalScoperesolved for the project and branch; -
setProjectName(),setBranch(),setId()andsetPath()onBaseUrlParams: usePortalUrlService.portalScope(); -
projectName()andbranch()onComponentUrlParams: a component URL addresses a component of the page the request renders, in that request’s project and branch.
Scripts and tracing
-
PortalScriptService.executeAsync()andScriptRuntime.executeAsync(): usebootstrap()for bootstrap scripts, orexecute()on an executor of your choice. -
ScriptSettings.Builder.globalVariable(),ScriptSettings.getGlobalVariables()andScriptTestSupport.addGlobalVariable(): custom JavaScript globals are phased out, andappis the only supported global. -
Trace.putAll()andclear(): record attributes with the typedattribute()methods;Tracewill stop extendingMap. -
getSize(),getBodyLength()andaddTraceInfo()ofBaseWebHandler: XP records the response trace info itself.
Behaviour changes
- URLs that do not resolve
-
pageUrl(),imageUrl(),attachmentUrl()andprocessHtml()generate a URL answered with 404, rather than one answered with 500, when the content it names does not resolve. See URLs that do not resolve. - Rich text images
-
processHtml()writessizesonly along with asrcsetof at least one width, and an image the image API serves as stored gets nosrcset.srcsetandsizescome right aftersrcin the<img>tag. The height of an image with an aspect ratio is rounded to the nearest pixel: a21:9image is now 329 pixels high at the default width of 768, rather than 324. - Rich text macros
-
processHtml()now looks up each macro itself, so macros render on any page, also outside a site. A macro no application provides is shown as written. HTML comments in the rich text stay plain comments: they are never run as macros.
Worth adopting
None of these are deprecations. They are places where 8.1 offers a supported way to do something apps have had to work around.
- Content Security Policy
-
A
Content-Security-Policyheader assembled by hand and set on the response is now folded into the request’s policy rather than sent verbatim, which means it is no longer the last word - a later contribution can add to it. Declare the policy throughcsp()instead: one policy is shared by the page, its layouts, its parts and any response processor, and the header is composed when the response is flushed. - Data down the request pipeline
-
Properties assigned to the request object -
req.myFlag = true- never reached anything downstream; the field is dropped when the request is handed on.setCustomLocalAttribute()stores the value in the current execution instead, where implementations, response processors and the code afternext()in a filter can all read it back. - Initialization guards
-
main.tsnow runs to completion before anything else in the app on the same node, so a flag that made every request check whether start-up had finished is no longer needed. Keep a guard for work that must happen once for the cluster - a node does not wait formain.tson any other node, so two nodes can still initialize the same repository at the same time. - Disposer registration
-
__.disposer()registrations are remembered when they are made while the application is starting - frommain.ts, or a module it loads during start-up. Registering one later is not dependable; move such registrations into start-up. - URLs for a frontend of your own
-
A frontend that serves content from a host of its own builds its URLs from the URL parts functions:
pageUrlParts(),imageUrlParts(),attachmentUrlParts()andprocessHtmlParts(), with the site or project resolved once byportalScope(). - Width of rich text images
-
The
srcof every image in rich text was 768 pixels wide.imageSrcWidthofprocessHtml()sets that width; the height still follows the image’s style. - Image styles of a given application
-
An image in rich text can name its style as
<application>:<name>, where several applications define styles of the same name.