| name | update-template-fo |
| description | Updates a Lutece FO (Front Office) template by replacing raw HTML with the FO FreeMarker macros from lutece-core. Use when the user asks to migrate, convert or update a skin/FO template with Lutece macros. |
| argument-hint | template-path |
Updating Lutece FO templates
You must update a Lutece FO template by replacing all raw HTML with the FO FreeMarker macros defined in lutece-core/webapp/WEB-INF/templates/skin/themes/.
Steps
- Read the template target provided by the user
- Identify all raw HTML elements replaceable by FO macros
- Consult the macros if needed by reading the definition files in
lutece-core/webapp/WEB-INF/templates/skin/themes/lutece/macros/
- Rewrite the template using FO macros exclusively
- Do not modify i18n files unless necessary and requested
HTML โ FO Macros mapping table
Structure and Layout
| HTML | Macro FO | Notes |
|---|
<div class="container"> | <@cContainer> | Can take class, type |
<div class="row"> | <@cRow> | Can take class, id |
<div class="col-..."> | <@cCol> | Use cols='12 col-md-X' |
generic <div> | <@cBlock> | type='div' by default |
<section> | <@cSection> | Dedicated macro. <@cBlock type='section'> also possible |
<article> | <@cArticle> | Dedicated macro |
<header> | <@cHeader> | Dedicated macro |
<aside> | <@cBlock type='aside'> | No dedicated macro โ use cBlock with type |
<footer> | <@cBlock type='footer'> | No dedicated macro โ use cBlock with type |
<main> | <@cBlock type='main'> | No dedicated macro โ use cBlock with type |
Text and Titles
| HTML | Macro FO | Notes |
|---|
<h1> to <h6> | <@cTitle level=N> | N = 1 to 6 |
<p> | <@cText> | type='p' by default |
<span> | <@cInline> | type='span' by default. Not self-closing โ always </@cInline> |
<em>, <strong>, <small> | <@cInline type='em'>, etc. | Via the type parameter |
<time datetime="..."> | <@cInline type='time' params='datetime="..."'> | No dedicated macro โ pre-build the date with <#assign> |
<i class="ti ti-xxx"> | <@cIcon name='xxx' /> | Prefer <@cIcon> โ shortcut with automatic ti ti- prefix |
Lists
| HTML | Macro FO | Notes |
|---|
<ul> | <@chList> | type='u' by default, type='o' for ordered |
<ol> | <@chList type='o'> | |
<li> | <@chItem> | |
Components
| HTML | Macro FO | Notes |
|---|
<div class="alert ..."> | <@cAlert> | Use type='warning', type='danger', etc. |
<div class="card ..."> | <@cCard> | Parameters: title, header, headerLevel, headerLabelClass, class, titleLevel, etc. |
<div class="modal ..."> | <@cModal> | |
<div class="accordion ..."> | <@cAccordion> | |
<div class="progress"> | <@cProgress> | Parameters: label, progressId, color, value, min, max, text |
Links and Buttons
| HTML | Macro FO | Notes |
|---|
<a href="..."> | <@cLink href='...' label='...'> | Standard link |
<a class="btn ..."> | <@cBtn href='...' class='...'> | Link styled as a button |
<button> | <@cBtn> | type='submit' by default |
| inline SVG/icon inside a button | Use <@cIcon> nested inside <@cBtn> | nestedPos='before' (default) or 'after' |
Images
| HTML | Macro FO | Notes |
|---|
<img> | <@cImg src='...' alt='...'> | class='img-fluid' by default |
<figure> + <figcaption> | <@cFigure caption='...'> + <@cImg> nested | The macro handles the <figcaption> via the caption parameter |
Forms
| HTML | Macro FO | Notes |
|---|
<form> | <@cForm> | method='post', action |
<input> | <@cInput> | |
<input type="hidden"> | <@cInput type='hidden' class='' /> | Always add class='' |
<input type="password"> | <@cInput type='password'> or <@cInputPassword> | cInputPassword for the full version with toggle |
<label> | <@cLabel> | |
| Label + Input grouped | <@cField label='...' required=true> | Prefer cField, use required=true instead of appending * to the label |
<input type="radio"> | <@cRadio> | name, label, value, checked |
<input type="checkbox"> | <@cCheckbox> | Params: name, label, value, checked, inline, required, disabled, params |
<select> | <@cSelect> | With <@cOption> nested. Supports errorMsg and helpMsg. Class: form-select (Bootstrap 5), do not use form-control |
<textarea> | <@cTextArea> | |
<fieldset> | <@cFieldset> | |
<div class="input-group"> |
Tables
| HTML | Macro FO | Notes |
|---|
<table> | <@cTable> | |
<thead> | <@cThead> | |
<tbody> | <@cTbody> | |
<tr> | <@cTr> | |
<th> | <@cTh> | |
<td> | <@cTd> | |
cTable โ chList + cCard (optional, on request)
A table listing entities can be replaced by a list of cards only if the user explicitly requests it. Do not apply it systematically during a template update.
Pattern: <@cTable> โ <@chList> + <@chItem> + <@cCard title=entityTitle>
<#if list_items?? && list_items?size gt 0>
<@chList>
<#list list_items as item>
<@chItem>
<@cCard title=item.title>
<@chList>
<@chItem><@cIcon name='calendar' /> ${item.date!}</@chItem>
<@chItem><@cIcon name='info' /> ${item.description!}</@chItem>
</@chList>
<#if item.actions?? && item.actions?size gt 0>
<@cRow class='mt-3'>
<@cCol>
<#list item.actions as action>
<@cBtn href='...' class='outline-secondary btn-sm me-1'>
...
</@cBtn>
</#list>
</@cCol>
</@cRow>
</#if>
</@cCard>
</@chItem>
</#list>
</@chList>
<#else>
<@cAlert type='warning' title='#i18n{portal.util.labelNoItem}' />
</#if>
Steps (multi-step forms)
| HTML | Macro FO | Notes |
|---|
| Completed step | <@cStepDone> | Params: step (required), title (required), idx (required), actionName, actionHref, actionLabel |
| Current step | <@cStepCurrent> | Params: step (required), title (required), showPrevStep, actionNextStep, actionPrevStep, hasMandatory, hasSteps |
| Upcoming step | <@cStepNext> | Params: step (required), title (required). Self-closing: <@cStepNext step='3' title='...' /> |
Mandatory conventions
Global structure
- Always wrap the template in
<@cTpl>...</@cTpl>
<@cContainer> is optional, use it only if the content requires a centered container
- You can go directly from
<@cTpl> to <@cCol>, <@cRow>, or <@cCard> as needed
- For full-page forms:
<@cTpl> โ <@cCol> โ <@cForm> โ <@cRow> โ <@cCol> โ content
cCol - Column format
- Use the format
cols='12 col-md-X' (not cols='xs-12 col-md-X' โ the xs- prefix no longer exists in Bootstrap 5)
- Replace
cols='xs-12 ...' with cols='12 ...' systematically
- Replace
<@cCol cols='12'> with <@cCol> โ full-width column by default, no need for cols
- For class only:
<@cCol class='12 col-md-6'>
- Extra utility classes go in
class: <@cCol cols='12 col-md-6' class='pt-5 mt-5'>
cAlert - Alerts
- Use the
type parameter: <@cAlert type='warning'>, <@cAlert type='danger'>
- Inline icon SVGs are unnecessary, the macro handles the display
- The
title parameter lets you add a title to the alert
cInput - Hidden fields
- Always add
class='' on hidden inputs: <@cInput type='hidden' name='x' value='y' class='' />
cIcon - Tabler icons
- Prefer
<@cIcon> over <@cInline type='i' class='ti ti-xxx' />
- The
ti ti- prefix is added automatically: <@cIcon name='eye' /> โ <span class="ti ti-eye">
- Extra classes via
class: <@cIcon name='settings' class='me-1' />
- By default
name='check': <@cIcon /> displays the check icon
cLabel - Labels
- Remove obsolete Bootstrap 3 classes:
col-xs-12, col-sm-*, control-label
- If the only class is
control-label or col-xs-12 control-label, remove the class parameter entirely: <@cLabel for='...'>
- The macro handles the label styling itself
Bootstrap 3 โ Bootstrap 5 classes
help-block โ form-text (help text under a field)
control-label โ remove (handled by the macro)
col-xs-* โ col-* (the xs breakpoint no longer exists in BS5)
has-error โ is-invalid (validation)
btn-default โ btn-secondary
FreeMarker HTML entities
- Replace
> with gt in FreeMarker conditions: <#if list?size gt 0> (not >)
- Replace
< with lt in FreeMarker conditions: <#if value lt 10> (not <)
FreeMarker ternary operator
cField - Fields with label
- Prefer
<@cField> to group a label and an input rather than cBlock + cLabel + cInput manually
- Use
required=true for mandatory fields โ do not append * manually to the label
- Do not use
for โ the macro handles the label/input link
- Can contain a nested
<@cInputGroup> for fields with addons (password toggle, generator, etc.)
cInputGroup - Input groups
- Replaces
<div class="input-group">
- Contains a
<@cInput> and one or more <@cBtn> directly nested
- Do not use
<@cInputGroupAddonText> to wrap the buttons
cProgress - Progress bar
label (required): text displayed above the bar
progressId: ID of the bar (used by JS for DOM manipulation)
color: Bootstrap color ('primary', 'danger', 'warning', etc.)
value: initial value (0 by default)
cBtn - Buttons
- The class is prefixed automatically with
btn btn-: class='primary' โ class="btn btn-primary"
label is a mandatory parameter โ always specify it, even when the content is nested:
- With nested content (icon + text):
label=''
- With text only:
label='My text'
- For a link-button: add
href='...'
- For sizes: include in class:
class='outline-primary btn-lg'
- For icon + text nested:
label='' with icon and text nested
- For a discreet link without border:
class='link border-0' (not outline-dark)
- Self-closing when there is no nested content:
<@cBtn label='My text' ... /> (no </@cBtn>)
cCard - Cards
- Use
header for the header text, headerLevel for its heading level (0 = span, >0 = hN)
headerLabelClass to style the header: e.g. 'text-danger fw-bold h2'
title for the main title (rendered in card-body), titleLevel and titleClass for styling
- Add
class='border border-danger' for colored borders
cInput - errorMsg and helpMsg
errorMsg: error message displayed under the field โ automatically adds the is-invalid class and aria-invalid. Pass an empty string if no error.
helpMsg: help text displayed under the field. Use ?then() to show the help only if there is no error:
<@cInput ... errorMsg=formGroupError helpMsg=(formGroupError != '')?then('', formMessages.fieldHelp!) />
- Replace the patterns
<#if formGroupError != ''>${formGroupError}<#elseif ...><@cInline class='form-text'>...</@cInline></#if> with these parameters
- Adapt
formGroupError: store the raw error text (not the HTML span) so it can be passed to errorMsg:
<#-- INCORRECT -->
<#assign formGroupError = '<span class="form-text text-danger">${form_error.errorMessage}</span>' />
<#-- CORRECT -->
<#assign formGroupError = form_error.errorMessage />
cInput - Native size and validation parameters
cInput - Extra HTML attributes
- Use
params for attributes not covered by the macro parameters: params='onkeypress="return fn(event);"'
- Dynamic validation classes: always include
form-control: class='form-control ${classPassword?if_exists}'
Macro parameters - Complex dynamic values
- Never inline FreeMarker logic (
<#if>, <#list>, complex interpolations) directly in a macro parameter โ this applies to all parameters, not only params
- Use
<#assign> (block syntax or directive) to pre-build the value before the call:
<#-- INCORRECT โ inline FreeMarker in actionHref -->
<@cStepDone actionHref='jsp/site/Portal.jsp?id=${form.id}<#if condition>&ref=${ref}</#if>' ...>
<#-- CORRECT โ block assign before the macro -->
<#assign stepTwoHref>jsp/site/Portal.jsp?id=${form.id}<#if condition>&ref=${ref}</#if></#assign>
<@cStepDone actionHref=stepTwoHref ...>
- For HTML attributes via
params, same rule:
<#assign btnTitle = '#i18n{label.lastLogin} : '>
<#if user.getLastLogin()?has_content>
<#assign btnTitle = btnTitle + user.getLastLogin()>
<#else>
<#assign btnTitle = btnTitle + '#i18n{label.never}'>
</#if>
<@cBtn params='title="${btnTitle}"' ... />
- This avoids quote-escaping problems (
') and ParseExceptions
cImg - Images
class='img-fluid' is applied by default, no need to specify it
- Extra HTML attributes via
params: params='width="72"'
chList / chItem - Styled lists
- For Bootstrap lists:
<@chList class='list-group'> + <@chItem class='list-group-item'>
- For navs:
<@chList class='nav ms-auto'>
cCheckbox - Checkboxes
cStepDone / cStepCurrent / cStepNext - Multi-step forms
- Replace the
<div class="row nextStepTitleRow">, <div class="row currentStepTitleRow"> and <div class="row currentStepContentRow">
<@cStepDone>: completed step, displays a check and a summary. The nested content is the step summary.
<@cStepDone step='1' title='Step 1 title' idx=0>
Summary of the completed step
</@cStepDone>
<@cStepCurrent>: current step, contains the active form/content nested.
<@cStepCurrent step='2' title='Step 2 title' showPrevStep=false hasMandatory=false>
...step content (alerts, form, picker, etc.)...
</@cStepCurrent>
<@cStepNext>: upcoming step, self-closing, no nested content.
<@cStepNext step='3' title='#i18n{...}' />
- Never inline a FreeMarker condition in the
title parameter of the cStep* macros โ use an <#assign> variable defined inside <@cTpl> (just after line 1) and pass it without quotes:
<@cTpl>
<#assign stepFormTitle><#if form.title != "">${form.title}<#else>#i18n{...default}</#if></#assign>
<@cStepDone step='1' title=stepFormTitle idx=0>
...
</@cStepDone>
- The
<#assign> always go inside <@cTpl>, never before โ <@cTpl> must be on line 1 of the file, the assigns on the following lines
cForm - Forms
- Attributes not covered by the parameters via
params: params='name="createAccount"'
i18n
- All displayed text must use
#i18n{plugin.key}
- Do not write hardcoded text in the template
Code readability
- Expand the
<#list> with conditional logic across multiple lines, do not leave compact inline blocks when they contain nested <#if>
chList / chItem - Replacing orphan <li>
cInput hidden - Mandatory empty class
BO vs FO macros - Do not mix
- Never use BO macros (admin/Tabler) in an FO (skin) template. BO macros such as
<@messages>, <@aButton>, <@button>, <@box>, <@formGroup>, <@tform>, <@select>, <@option> are not available in the FO context
- BO โ FO equivalents:
cFieldset - Replacing fieldset/legend
form-group โ cRow/cCol
style attribute on macros
cols - Invalid formats
cols='xs-12 sm-12' โ <@cCol> (full width by default, no need for cols)
cols='xs-12 col-sm-6' โ cols='12 col-sm-6'
cols='12' alone โ remove the parameter, use <@cCol>
- The
xs- prefix does not exist in Bootstrap 5, always use the prefix-less form for mobile
FreeMarker conditions - Empty if branch
FreeMarker - Modern syntax (?? vs ?exists)
cSelect - Class and errorMsg/helpMsg parameters
- Never add
class='form-control' on <@cSelect> โ Bootstrap 5 uses form-select, but the macro handles the base class automatically
- For extra classes (validation), use
class='form-select ${entry.CSSClass!}' + (errorMsg != '')?then(' is-invalid', '')
<@cSelect> supports errorMsg and helpMsg exactly like <@cInput> โ pass the messages directly, no need for a separate <@cAlert>
<#-- INCORRECT โ class='form-control' + separate @cAlert -->
<@cSelect name='myField' class='form-control'>...</@cSelect>
<#if errorMsg != ''>
<@cAlert type='danger' title=errorMsg />
</#if>
<#-- CORRECT โ form-select + errorMsg/helpMsg directly on the macro -->
<#assign selectClass = 'form-select ${entry.CSSClass!}' + (errorMsg != '')?then(' is-invalid', '')>
<@cSelect name='myField' class=selectClass errorMsg=errorMsg helpMsg=helpMsg>...</@cSelect>
cOption - selected parameter
cAlert - Message list
cInline - Span / em / time / strong and other inlines
- Not self-closing: always requires a closing tag
</@cInline>, even when the content is empty
<#-- INCORRECT โ self-closing -->
<@cInline class='bl-marker' params='data-id="1"' />
<#-- CORRECT โ always close, even when empty -->
<@cInline class='bl-marker' params='data-id="1"'></@cInline>
- The
type parameter accepts any inline tag: 'span' (default), 'em', 'strong', 'small', 'time', 'cite', 'mark', 'kbd', 'code', etc.
- For HTML
<time>, pre-build the ISO date with <#assign> then inject it into params:
<#assign updateDateIso = blog.updateDate?string('yyyy-MM-dd')>
<@cInline type='time' params='datetime="${updateDateIso}"'>${blog.updateDate?string('d MMMM yyyy')}</@cInline>
The reverse โ params='datetime="${blog.updateDate?string("yyyy-MM-dd")}"' โ causes a ParseException because of the nested quotes.
- For
data-* attributes that contain an i18n key, pre-build with <#assign> as well:
<#assign label = "#i18n{plugin.key.label}">
<@cInline class='bl-target' params='data-label="${label}"'></@cInline>
cFigure - Figures with caption
Semantic HTML5 elements (article / header / section / aside)
<article> โ <@cArticle> (dedicated macro)
<header> โ <@cHeader> (dedicated macro)
<section> โ <@cSection> (dedicated macro) โ <@cBlock type='section'> also remains valid
<aside>, <footer>, <main>, <nav> โ <@cBlock type='aside'> (no dedicated macro, but cBlock accepts any type)
- All these macros accept
class, id, params like cBlock
Dynamic classes (conditional concatenation)
- Always pre-build the
class string with <#assign> rather than inline FreeMarker in the class parameter
<#-- INCORRECT โ inline FreeMarker in class -->
<@cBlock class='bl-body<#if !blog.displayToc> bl-body-one-col</#if>'>
<#-- CORRECT โ assign before the macro -->
<#assign bodyClass = 'bl-body'>
<#if !blog.displayToc><#assign bodyClass = bodyClass + ' bl-body-one-col'></#if>
<@cBlock class=bodyClass>
- The
?then(a, b) pattern is also acceptable for only 1 or 2 classes:
<#assign cardClass = 'bl-card' + isActive?then(' is-active', '')>
Clickable links styled as a card (not as a button)
Empty lists filled by JS
Dead / duplicated code
- During a migration, always re-read the result to detect any buggy copy-paste (e.g. a duplicated
<#assign breadcrumbItems...> in another container without use)
- Remove commented-out HTML blocks (
<!-- ... -->) that are not genuinely useful as documentation
- Remove
<!-- TOC -->, <!-- BODY --> etc. comments whose intent is obvious in the structured FreeMarker code
jQuery โ Vanilla JS - Mandatory conversion
The jQuery library is no longer loaded by the theme. Any JavaScript using $(...), jQuery(...) or jQuery plugins must be systematically rewritten in vanilla JS when migrating a template โ it is non-negotiable, otherwise the code breaks at runtime.
Standard mapping of the most common jQuery operations:
| jQuery | Vanilla JS |
|---|
$('#foo'), $('.bar') | document.querySelector('#foo'), document.querySelector('.bar') (1st match) |
$('.bar') (collection) | document.querySelectorAll('.bar') |
$el.find('.x') | el.querySelector('.x') or el.querySelectorAll('.x') |
$el.children('.x') | el.querySelectorAll(':scope > .x') |
$el.parent() | el.parentElement |
$el.closest('.x') | el.closest('.x') (already native) |
$el.each(fn) | nodeList.forEach(fn) (on NodeList or Array.from(htmlCollection)) |
$el.addClass('x'), .removeClass('x'), .toggleClass('x') | el.classList.add('x'), .remove('x'), .toggle('x') |
$el.hasClass('x') | el.classList.contains('x') |
$el.attr('foo', 'bar') | el.setAttribute('foo', 'bar') |
$el.attr('foo') (read) | el.getAttribute('foo') |
$el.removeAttr('foo') | el.removeAttribute('foo') |
$el.data('foo') | el.dataset.foo |
$el.text(), $el.text('...') | el.textContent (read/write) |
$el.html(), $el.html('...') | el.innerHTML (read/write) |
| , |
Recurring patterns to factor out into helpers when used several times in the same <script>:
function setDisabled(btn, value) {
if (!btn) return;
if (value) {
btn.classList.add('disabled');
btn.setAttribute('disabled', 'disabled');
} else {
btn.classList.remove('disabled');
btn.removeAttribute('disabled');
}
}
Mandatory safeguards:
- Always check the existence of the element after
querySelector: if (!el) return; or if (el) { ... } โ querySelector returns null if not found, el.classList.add(...) then crashes whereas $el.addClass(...) was silent on an empty collection.
- Prefer
event.key over event.which (deprecated) or event.keyCode (deprecated).
- Catch dead jQuery code: some jQuery selectors are poorly written (e.g.
$el.children('.a .b') which never matches โ .children() filters direct children with a simple selector). When converting, flag the presumed intent to the user rather than literally translating a no-op.
cText - Correct usage
What NOT to do
- Do not add JavaScript unless requested or required by a macro
- Do not use deprecated macro parameters
- Do not wrap a
<@cAlert> in an unnecessary <@cBlock> or <@cCard>
- Do not duplicate the
btn btn- prefix in the class of <@cBtn>
- Do not leave orphan
<li> without a parent <@chList>
- Do not wrap each
<@chItem> in a <@cRow>/<@cCol> โ list items go directly inside <@chList>
- Do not leave raw HTML tags (
<br>, <hr>, <b>, <i>, etc.) when a macro exists or when they are unnecessary โ remove formatting <br>
- Do not use
<@cCol cols='xs-12'> โ simply use <@cCol> (full-width column by default)
- Do not use
โ replace with a normal space or remove if unnecessary
- Do not use
style='...' on macros โ use class with Bootstrap utilities or params='style="..."' as a last resort
- Do not mix BO and FO macros โ check that all the macros used exist in the skin/FO context
- Do not use
> / < in FreeMarker conditions โ use gt / lt
- Do not self-close
<@cInline> โ always </@cInline>, even when the content is empty
- Do not inline FreeMarker in the
class parameter of a macro โ pre-build the string with <#assign> (also applies to href, id, etc.)
- Do not inline
?string('yyyy-MM-dd') directly in params='datetime="..."' โ nested quotes break the FreeMarker parser. Pre-build with <#assign>.
- Do not keep a separate
<figcaption> โ use the caption parameter of
Macro files reference
The definitions are located in:
- Components:
lutece-core/webapp/WEB-INF/templates/skin/themes/lutece/macros/components/
- Elements:
lutece-core/webapp/WEB-INF/templates/skin/themes/lutece/macros/elements/
- Forms:
lutece-core/webapp/WEB-INF/templates/skin/themes/lutece/macros/forms/
- Layout:
lutece-core/webapp/WEB-INF/templates/skin/themes/lutece/macros/layout/
- Utilities:
lutece-core/webapp/WEB-INF/templates/skin/themes/lutece/macros/utilities/
If in doubt about a macro's parameters, read the corresponding .ftl file to check the signature and documentation.
Reference examples
Typical error page
<#include "minimal_header.html" />
<@cTpl>
<@cContainer class='vh-80 pt-5'>
<@cRow class='pt-5 mt-5'>
<@cCol cols='12 col-md-3' class='pt-5 mt-5'>
<@cImg src='themes/skin/shared/images/500.png' alt='#i18n{portal.util.error500.title}' id='error500-img' />
</@cCol>
<@cCol cols='12 col-md-6' class='pt-5 mt-5'>
<@cCard class='border border-danger mt-5' header='Error 500' headerLevel=1 headerLabelClass='text-danger fw-bold h2' title='#i18n{portal.util.error500.title}' titleClass='h2' titleLevel=2>
<@cText class='my-5 fs-2'>#i18n{portal.util.error500.text}</@cText>
<#if error_cause??>
<@cAlert type='danger' class='fs-3'>${error_cause}</@cAlert>
</#if>
<@cText class='text-center mt-5'>
<@cBtn href='./' label='#i18n{portal.util.labelBackHome}'>
<@cIcon name='home' />
</@cBtn>
</@cText>
</@cCard>
</@cCol>
</@cRow>
</@cContainer>
</@cTpl>
<#include "minimal_footer.html" />
Typical choice list
<@cTpl>
<@cRow>
<@cCol>
<@cTitle level=2>#i18n{mylutece.xpage.create_account.pageTitle}</@cTitle>
<#if list_authentications?has_content>
<@cText>#i18n{mylutece.xpage.create_account.contentMessage}</@cText>
<@chList class='list-group'>
<#list list_authentications as authentication>
<@chItem class='list-group-item'>
<@cLink href='${authentication.newAccountPageUrl}' label='${authentication.authServiceName!}' title='${authentication.authServiceName!}' nestedPos='before'>
<@cImg src='${authentication.iconUrl!}' alt='${authentication.authServiceName!}' />
</@cLink>
</@chItem>
</#list>
</@chList>
</#if>
<@cAlert type='warning' title='#i18n{mylutece.xpage.create_account.noAuthentication}' />
</@cCol>
</@cRow>
</@cTpl>
Typical registration form (with input-group and progress)
<@cTpl>
<@cRow>
<@cCol cols='12 col-md-4 offset-md-4'>
<#if error_code?has_content>
<@cAlert type='danger'>#i18n{...errorMessage}</@cAlert>
</#if>
<@cTitle level=2>#i18n{...pageTitle}</@cTitle>
<@cForm id='createAccount' action='...' method='post' params='name="createAccount"'>
<@cInput type='hidden' name='plugin_name' value='${plugin_name}' class='' />
<@cField label='#i18n{...email}' required=true>
<@cInput type='text' name='email' id='email' class='form-control ${classEmail?if_exists}' params='maxlength="100"' value='${(user.email)?if_exists}' />
</@cField>
<@cField label='#i18n{...password}' required=true>
<@cInputGroup>
<@cInput type='password' id='password' name='password' class='form-control ${classPassword?if_exists}' params='maxlength="100"' />
<@cBtn href='#' class='secondary btn-sm p-2' id='lutece-password-toggler' label='' params='title="Show / hide the password"'>
<@cIcon name='eye' />
</@cBtn>
<@cBtn href='#' class='secondary btn-sm p-2' id='generate_password' label='' params='title="Generate a password"'>
<@cIcon name='settings' class='me-1' />
<@cInline class='d-none'>Generate a password</@cInline>
</@cBtn>
</@cInputGroup>
</@cField>
<@cBlock class='py-3'>
<@cProgress label='#i18n{...passwordComplexity}' progressId='progress_bar_first_password' color='danger' value=0 />
</@cBlock>
<@cRow>
<@cCol>
<@cBtn class='primary' type='submit' label='' params='name="createAccountBtn"'>
<@cIcon name='user-check' /> #i18n{...btnCreateAccount}
</@cBtn>
<@cBtn class='secondary' type='button' label='' params='name="back" onclick="javascript:history.go(-1)"'>
<@cIcon name='circle-x' /> #i18n{...btnBack}
</@cBtn>
</@cCol>
</@cRow>
</@cForm>
</@cCol>
</@cRow>
</@cTpl>
Typical login form
<@cTpl>
<@cCol>
<@cForm method='post' action='${url_dologin}'>
<@cInput type='hidden' name='page' value='mylutece' class='' />
<@cInput type='hidden' name='action' value='doLogin' class='' />
<@cInput type='hidden' name='token' value='${token}' class='' />
<@cRow class='mt-xxl'>
<@cCol cols='12 col-md-6' class='mt-xxl'>
<#if error_message?? && error_message != ''>
<@cAlert type='warning' title='${error_message!}' />
</#if>
<@cCard title='#i18n{mylutece.xpage.login_form.pageTitle}' class='my-l'>
<@cField label='#i18n{mylutece.xpage.login_form.labelAccessCode}' for='username'>
<@cInput type='text' name='username' id='username' placeholder='name@example.com' />
</@cField>
<@cField label='#i18n{mylutece.xpage.login_form.labelPassword}' for='password'>
<@cInput type='password' name='password' id='password' placeholder='#i18n{mylutece.xpage.login_form.labelPassword}' />
</@cField>
<@cBtn class='primary w-100 py-m mt-l' type='submit' label='#i18n{mylutece.xpage.login_form.labelButton}' />
<@cRow class='justify-content-center mt-l'>
<@cCol class='d-flex justify-content-end'>
<@cBtn href='${lostPasswordUrl!}' label='' params='title="..."'>
<@cIcon name='password-user' /> #i18n{...labelButtonLostPassword}
</@cBtn>
</@cCol>
</@cRow>
</@cCard>
</@cCol>
<@cCol cols='12 col-md-3' class='mt-xxl'>
<@cImg src='themes/skin/lutece/images/signin.png' alt='#i18n{mylutece.xpage.login_form.labelButton}' />
</@cCol>
</@cRow>
</@cForm>
</@cCol>
</@cTpl>
Typical multi-step recap page (with cStepDone, cStepCurrent, cStepNext)
<@cStepDone step='1' title='#i18n{...stepOneTitle}' idx=0>
${form.description!}
</@cStepDone>
<@cStepDone step='2' title='#i18n{...stepTwoTitle}' idx=1 actionHref='jsp/site/Portal.jsp?page=appointment&view=getViewAppointmentCalendar&id_form=${form.idForm}' actionLabel='#i18n{portal.util.labelModify}'>
<@chList>
<@chItem>#i18n{...labelDate} ${appointment.dateOfTheAppointment}</@chItem>
</@chList>
</@cStepDone>
<@cStepDone step='3' title='#i18n{...stepThreeTitle}' idx=2 actionHref='javascript:history.back()' actionLabel='#i18n{portal.util.labelModify}'>
<@chList>
<@chItem>${formMessages.fieldLastNameTitle!} : ${appointment.lastName}</@chItem>
<@chItem>${formMessages.fieldFirstNameTitle!} : ${appointment.firstName}</@chItem>
<@chItem>${formMessages.fieldEmailTitle!} : ${appointment.email}</@chItem>
<#list listResponseRecapDTO as response>
<#if response.recapValue?? && response.recapValue?has_content>
<@chItem>${response.entry.title} : ${response.recapValue}</@chItem>
</#if>
</#list>
</@chList>
</@cStepDone>
<@cStepCurrent step='4' title='#i18n{...validationTitle}' hasMandatory=false>
<@cForm action='jsp/site/Portal.jsp' method='post'>
<@cInput type='hidden' name='page' value='appointment' class='' />
<@cInput type='hidden' name='action' value='doMakeAppointment' class='' />
<@cInput type='hidden' name='token' value='${token}' class='' />
<@cText>#i18n{...validationText}</@cText>
<@cBtn type='submit' class='primary'>
<@cIcon name='check' /> #i18n{...labelValidate}
</@cBtn>
</@cForm>
</@cStepCurrent>
<@cStepNext step='5' title='#i18n{...confirmationTitle}' />
Article / rich content page (semantic HTML5 + dynamic breadcrumb)
Recommended pattern for an article detail page (blog, news, etc.) with:
- Breadcrumb built dynamically from URL parameters
- Header with metadata (tags, date, reading time)
- Hero image via
<@cFigure caption=...>
- Aside with table of contents (TOC)
- Related articles section at the bottom
<@cTpl>
<#assign readingTimeLabel = "#i18n{plugin.readingTime.label}">
<@cContainer>
<@cRow>
<@cCol>
<@cArticle class='bg-light'>
<#-- Dynamic breadcrumb built from the received params -->
<#assign breadcrumbItems = []>
<#if from_page_name?? && from_page_name != ''>
<#assign fromPageUrl = ''>
<#if from_page_id??><#assign fromPageUrl = 'jsp/site/Portal.jsp?page_id=' + from_page_id?c></#if>
<#assign breadcrumbItems = breadcrumbItems + [{ 'title': from_page_name, 'url': fromPageUrl }]>
</#if>
<@cBreadCrumb home='Home' type='fluid' items=breadcrumbItems />
<@cHeader class='hero'>
<@cBlock>
<@cBlock class='hero__meta'>
<#if blog.tag?has_content>
<#list blog.tag as tg>
<@cInline class='tag'>${tg.name}</@cInline>
</#list>
</#if>
<@cInline>ยท</@cInline>
<#if blog.updateDate??>
<#assign dateIso = blog.updateDate?string('yyyy-MM-dd')>
<@cInline type='time' params='datetime="${dateIso}"'>${blog.updateDate?string('d MMMM yyyy')}</@cInline>
</#if>
<@cInline>ยท</@cInline>
<@cInline class='reading-time' params='data-reading-time-label="${readingTimeLabel}"'></@cInline>
</@cBlock>
<@cTitle level=1 class='hero__title'>${blog.contentLabel}</@cTitle>
<@cText class='hero__lede'>${blog.description!}</@cText>
</@cBlock>
<#if blog.docContent?? && blog.docContent?size != 0>
<#list blog.docContent?sort_by('priority') as doc>
<#if doc.contentType.idContentType == 1>
<@cFigure class='hero__img' caption=blog.contentLabel>
<@cImg src='servlet/plugins/blogs/file?id_file=${doc.id!}' alt=blog.contentLabel />
</@cFigure>
<#break>
</#if>
</#list>
</#if>
</@cHeader>
<#assign bodyClass = 'body'>
<#if !blog.displayToc><#assign bodyClass = bodyClass + ' body--one-col'></#if>
<@cBlock class=bodyClass>
<#if blog.displayToc>
<@cBlock type='aside' class='toc'>
<@cBlock class='toc__title'>#i18n{plugin.tocTitle}</@cBlock>
<@chList id='toc-list'></@chList>
</@cBlock>
</#if>
<@cBlock class='article-content'>
${blog.htmlContent}
</@cBlock>
</@cBlock>
</@cArticle>
<#if blog.displayRelated && related_blogs?? && related_blogs?size gt 0>
<@cSection class='related'>
<@cBlock class='related__title'>#i18n{plugin.relatedTitle}</@cBlock>
<@cBlock class='cards'>
<#list related_blogs as relBlog>
<#assign relUrl>jsp/site/Portal.jsp?page=blog&id=${relBlog.id}<#if blog.attachedPortletId gt 0>&portlet_id=${blog.attachedPortletId}</#if></#assign>
<@cLink href=relUrl class='card' label=''>
<@cBlock class='card__body'>
<@cTitle level=3>${relBlog.contentLabel}</@cTitle>
<@cText>${relBlog.description!}</@cText>
</@cBlock>
</@cLink>
</#list>
</@cBlock>
</@cSection>
</#if>
</@cCol>
</@cRow>
</@cContainer>
</@cTpl>
Key points of this pattern:
<@cArticle>, <@cHeader>, <@cSection>, <@cBlock type='aside'> for HTML5 semantics
<#assign> blocks to pre-build URLs, ISO dates and dynamic class names (never inline FreeMarker in macro parameters)
<@cInline type='time'> for the <time> tag (no dedicated macro)
<@cFigure caption=...> rather than separate <figure> + <figcaption>
<@cLink class='card' label=''> for clickable cards (not <@cBtn>)
<@chList id='...'></@chList> for an empty list to fill on the JS side