MARLO’s frontend is a server-rendered, pull-based MVC system. The Struts 2 action populates the model, and FreeMarker renders it into HTML. There is no client-side routing, no single-page application framework, and no virtual DOM — the server produces complete HTML pages, and the browser loads them in full. Understanding how the templates are composed is the prerequisite for modifying any screen or adding a new one.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/CCAFS/MARLO/llms.txt
Use this file to discover all available pages before exploring further.
Three composition mechanisms
FreeMarker composition in MARLO uses three distinct mechanisms, each with a specific role:[#include]— pulls in layout fragments (header, menu, footer, messages, breadcrumb). The included fragment has access to the surrounding template’s variables.[#import ... as alias]— loads a macro library and binds it to an alias. The alias is used for all macro calls from that library.- Macro calls (
[@alias.macroName ... /]) — render a reusable UI component with its parameters.
Standard layout fragments
Every full-page template includes the same set of global layout fragments from/WEB-INF/global/pages/. The canonical sequence from projectDescription.ftl:
| Fragment | Path | Purpose |
|---|---|---|
header.ftl | /WEB-INF/global/pages/header.ftl | HTML <head>, global CSS/JS imports, imports forms.ftl as customForm |
main-menu.ftl | /WEB-INF/global/pages/main-menu.ftl | Top navigation bar and CRP switcher |
footer.ftl | /WEB-INF/global/pages/footer.ftl | Page footer, closing <body> and </html> |
breadcrumb.ftl | /WEB-INF/global/pages/breadcrumb.ftl | Breadcrumb trail (rendered from the breadCrumb variable set in the page template) |
| Domain menu | e.g., /WEB-INF/crp/views/projects/menu-projects.ftl | Left-hand section navigation for the current domain |
| Domain messages | e.g., /WEB-INF/crp/views/projects/messages-projects.ftl | Section-level save status, validation messages |
The customForm macro library
header.ftl imports forms.ftl and binds it to the alias customForm:
header.ftl is included before the page body, customForm is available in every template that includes the standard header. You do not re-import it in individual page templates.
The macro library lives at marlo-web/src/main/webapp/WEB-INF/global/macros/forms.ftl and provides:
| Macro | Usage |
|---|---|
customForm.input | Single-line text, number, or email input |
customForm.textArea | Multi-line text area, optionally with rich-text editor |
customForm.select | Dropdown select bound to a Java collection |
customForm.checkbox | Single checkbox |
customForm.radioButtonGroup | Group of radio buttons |
Using customForm macros
Input names in MARLO mirror the Struts OGNL model path — the same string that Struts uses to bind the form field to the action property. This is how thecustomForm.input call renders the project title field in projectDescription.ftl:
name— the OGNL path. Struts uses this for both reading the current value from the model and binding the submitted value back to the action.i18nkey— key in the global or CRP-specific.propertiesfile for the field label.required=true— renders the required indicator and adds arequiredCSS class for client-side validation.editable— whenfalse, the macro renders the value as read-only text and a hidden input (the field is still submitted but not editable).
Domain-specific macro libraries
BeyondcustomForm, some domains import their own macro libraries for complex repeated components. Examples from the composition map:
POWB financial plan
Home dashboard
Template macro pattern for dynamic lists
When a section has a repeatable block — a list of partners, a list of funding sources — the pattern is to define a macro for a single row and render an invisible template instance for JavaScript to clone:isTemplate=true so it gets the ID suffix T. JavaScript (autoSave.js and section-specific scripts) clones this node, replaces T with the real index, and appends it to the list. This is the mandatory pattern for all dynamic list blocks — do not build ad-hoc DOM construction in JavaScript.
JavaScript integration
Three JavaScript files are wired into the standard page lifecycle:| File | Purpose |
|---|---|
autoSave.js | Polls for unsaved changes and triggers the save pipeline; manages the save-button state |
fieldsValidation.js | Client-side pre-flight validation that mirrors the server-side validator rules; prevents obvious invalid submits |
discardChangesPopup.ftl | FTL partial that renders the discard-changes confirmation modal; included by section-specific templates |
customJS array at the top of the page template:
header.ftl iterates customJS and emits the script tags.
State model: server state is canonical
The server holds the canonical state. When the browser loads a form, FreeMarker reads the model from the Struts action and populates every field. When the user submits, Struts binds the POST body back to the action model, runs the interceptor stack and validator, and persists via the Manager. The browser has no authoritative copy of the data at any point. TheautoSave.js client state — the in-memory representation of what the user typed since the last server save — is transient. It is used only to detect unsaved changes and show the discard-changes prompt. It is discarded on the next full page load.
MARLO has no single-page application, no client-side router, and no AJAX data-binding framework. Every save is a full form POST to a Struts
.do action. The Post-Redirect-Get pattern means a successful save always reloads the page from a GET. Do not introduce components that assume persistent client-side state across page navigations.Specificity-gated content
Sections that depend on a feature flag useaction.hasSpecificities(key) directly in the FTL:
APConstants and the row in the parameters table. See AGENTS.md for the full specificity implementation guide.
Constitutional rules for new UI work
Composition checklist for a new page
When adding a new full-page FTL template:- Open with
[#ftl]and settitle,customJS,customCSS, andbreadCrumbvariables. - Include
header.ftl(which makescustomFormavailable) andmain-menu.ftl. - Include the domain-specific
menu-<area>.ftlandmessages-<area>.ftlpartials. - Wrap form content in
[@s.form action=actionName method="POST" ...]. - Use
[@customForm.input],[@customForm.textArea],[@customForm.select]for all form controls. - Use
isTemplate=truefor any repeatable dynamic block. - Import domain macro libraries only after
header.ftlhas been included. - Close with
[#include "/WEB-INF/global/pages/footer.ftl" /].