Skip to content

8.x Make SiteMesh 3 the default GSP layout#15713

Open
codeconsole wants to merge 29 commits into
apache:8.0.xfrom
codeconsole:feat/sitemesh3-default-layout
Open

8.x Make SiteMesh 3 the default GSP layout#15713
codeconsole wants to merge 29 commits into
apache:8.0.xfrom
codeconsole:feat/sitemesh3-default-layout

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

Makes SiteMesh 3 the default GSP layout in generated applications, mutually exclusive with the legacy SiteMesh 2 grails-layout feature.

Introduces a GspLayout one-of feature group (enforced by OneOfFeatureValidator):

  • GspLayout — abstract one-of parent (Category.VIEW, WEB/WEB_PLUGIN)
  • Sitemesh3 — default member; auto-applied unless another GspLayout is selected; adds grails-sitemesh3
  • GrailsLayout — opt-in member; adds grails-layout (SiteMesh 2)

GrailsGsp is intentionally not modified: its existing if (!isFeaturePresent(Sitemesh3)) guard already skips grails-layout when SiteMesh 3 applies, so SiteMesh 2 remains available via GrailsLayout. Selecting both sitemesh3 and grails-layout now fails fast.

Depends on #15710

Split out from #15710 per review feedback ("making this the default & updating to sitemesh 3 should be separate PRs"). This branch is stacked on #15710, so until #15710 merges this PR's diff also shows the enable-SiteMesh-3 commits; it will reduce to just the make-default change once #15710 lands. Please merge #15710 first.

…s-layout

Introduce a GspLayout one-of feature group so SiteMesh 3 (grails-sitemesh3)
and the legacy SiteMesh 2 grails-layout are both selectable but never
applied together (enforced by OneOfFeatureValidator):

- GspLayout: abstract OneOfFeature parent (Category.VIEW), WEB/WEB_PLUGIN
- Sitemesh3: default member; auto-applied unless another GspLayout is
  selected; adds grails-sitemesh3
- GrailsLayout: opt-in member; adds grails-layout (SiteMesh 2)

The GspLayout features now own the layout dependency, so GrailsGsp's
'if (!isFeaturePresent(Sitemesh3)) add grails-layout' block is removed.
Selecting both sitemesh3 and grails-layout now fails fast.
@jamesfredley jamesfredley self-requested a review June 3, 2026 15:06
@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 32.22222% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.2569%. Comparing base (1c2d8a6) to head (536da4d).
⚠️ Report is 24 commits behind head on 8.0.x.

Files with missing lines Patch % Lines
...ils/plugins/web/taglib/RenderSitemeshTagLib.groovy 0.0000% 54 Missing ⚠️
...ils/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy 0.0000% 3 Missing ⚠️
...rails/plugins/sitemesh3/Sitemesh3LayoutFinder.java 60.0000% 2 Missing ⚠️
...3/GrailsSiteMeshViewResolverBeanPostProcessor.java 83.3333% 0 Missing and 1 partial ⚠️
...rails/plugins/sitemesh3/Sitemesh3CapturedPage.java 92.3077% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15713        +/-   ##
==================================================
- Coverage     48.3255%   48.2569%   -0.0687%     
- Complexity      15216      15236        +20     
==================================================
  Files            1875       1877         +2     
  Lines           85818      86023       +205     
  Branches        14969      15007        +38     
==================================================
+ Hits            41472      41512        +40     
- Misses          37975      38135       +160     
- Partials         6371       6376         +5     
Files with missing lines Coverage Δ
...lugins/sitemesh3/CaptureAwareContentProcessor.java 62.5000% <100.0000%> (+1.6304%) ⬆️
...s/plugins/sitemesh3/GrailsSiteMeshViewContext.java 50.0000% <100.0000%> (+10.0000%) ⬆️
.../plugins/sitemesh3/GrailsSiteMeshViewResolver.java 100.0000% <ø> (ø)
.../plugins/sitemesh3/Sitemesh3AutoConfiguration.java 0.0000% <ø> (ø)
.../plugins/sitemesh3/Sitemesh3RenderViewMutator.java 100.0000% <100.0000%> (ø)
...3/GrailsSiteMeshViewResolverBeanPostProcessor.java 90.0000% <83.3333%> (-10.0000%) ⬇️
...rails/plugins/sitemesh3/Sitemesh3CapturedPage.java 52.0000% <92.3077%> (+31.7101%) ⬆️
...rails/plugins/sitemesh3/Sitemesh3LayoutFinder.java 44.0000% <60.0000%> (+0.5657%) ⬆️
...ils/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy 0.0000% <0.0000%> (ø)
...ils/plugins/web/taglib/RenderSitemeshTagLib.groovy 0.0000% <0.0000%> (ø)

... and 8 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jamesfredley

Copy link
Copy Markdown
Contributor

Gaps to review and potentially address before SM3 becomes default

Gap / Open item SM2 / Current state SM3 / Needed
g:applyLayout attributes (template,url,action,controller,params,model,contentType,encoding,parse) Full support - RenderGrailsLayoutTagLib.groovy:100-173 name + body only; other attrs silently ignored - RenderSitemeshTagLib.groovy:77-106
Default-layout config + implicit application fallback Reads grails.views.layout.default (LayoutGrailsPlugin:37); falls back to layout application (GroovyPageLayoutFinder:180) Reads only grails.sitemesh.default.layout (Sitemesh3GrailsPlugin:83); no application fallback
Partial/template render suppression (GrailsRenderViewMutator + GrailsLayoutSelector) Registers both (LayoutGrailsPlugin:74-75); consumed by core ResponseRenderer:340-341,563 Registers neither → beans null → behavior lost
grails.views.layout.enable.nongsp (non-GSP/JSP layouts) LayoutGrailsPlugin:38 + GroovyPageLayoutFinder enableNonGspViews/viewMustExist Sitemesh3LayoutFinder:192-194 GSP locator only; flag unsupported
Captured-page isolation across nested/sibling applyLayout N/A (different mechanism) GrailsSiteMeshViewContext.dispatch:81 sets fresh page, no restore; taglib saves only LAYOUT_ATTRIBUTE (RenderSitemeshTagLib:78,100-104)
No-body captured page + layoutBody Handled CaptureAwareContentProcessor:82 returns used page without data; layoutBody reads only body (RenderSitemeshTagLib:218-225)
<title> strip robustness Regex-based, well-formed extractHead() matches "<title" as prefix - mis-slices <titlebar>/<title-x> (Sitemesh3CapturedPage.java:232)
Aggregate web starter still defaults to SM2 starter-web/build.gradle:53:grails-layout unless SITEMESH3_TESTING_ENABLED=true Default branch must be :grails-sitemesh3
~35 test examples default to SM2 else → grails-layout in every example (e.g. app1/build.gradle:42-47) Default = SM3; SM2 via forced lane
Main unit suite defaults to SM2 grails-test-suite-uber/build.gradle:53-58:grails-layout Default = SM3
SM2-only examples with no SM3 coverage Hardcoded grails-layout: test-phases:42, scaffolding:49, scaffolding-fields:38, jetty:41, database-cleanup:37, gsp-layout:50 Keep as SM2 anchors; add SM3 variants where parity matters
Default path ≠ tested path SITEMESH3_TESTING_ENABLED gates SM3; default build never runs SM3 Retire/invert flag so default == tested
No CI lane exercising SM3 as default Only opt-in env runs SM3 Add SM3-default lane + SM2 lane
Fragile upstream auto-config suppression Disabled NoopSitemeshFilter named sitemesh suppresses upstream SiteMeshAutoConfiguration (Sitemesh3GrailsPlugin:106-124) Robust suppression + guard test
Servlet 6.1 / Tomcat 11 forward→include dispatch SM3 bypasses RequestDispatcher.forward() for absolute layout paths (GrailsSiteMeshViewContext.java:88-93) Functional coverage on Tomcat 11
grails-gsp-spring-boot unpublished In settings.gradle:174, depends on SM3 (spring-boot/build.gradle:37), but absent from publish-root-config.gradle (only grails-sitemesh3 at :75) Publish or don't rely on it

Not gaps (verified parity)

Resolution order (6 steps), NONE_LAYOUT, layoutTitle/layoutHead, pageProperty/ifPageProperty, meta extraction, body.* attrs, grailsLayout:parameter, comma/chained decoration (happy path), unbounded layout cache (SM2 identical).

Minor / cosmetic

Cache-interval key differs: SM3 grails.sitemesh.layout.cache.interval vs SM2 grails.gsp.reload.interval.

@codeconsole codeconsole requested review from matrei and sbglasius June 3, 2026 21:44

@matrei matrei left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be a glitch in the UI when a default feature is replaced:

Image

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot about the test apps pointing at grails-layout by default. We'll need to update that as part of this switch and ensure no regressions.

@jamesfredley

Copy link
Copy Markdown
Contributor

I wish github didn't scroll on tables like that in comments, but there are a number of key features that will need to be added to the sitemesh 3 plugin to match sitemesh 2's historical feature set. When the tests are all run against sitemesh 3 it will surface most of these, but the comment above should be close to comprehensive.

…itch

Address review feedback on the SiteMesh 3 default change:

- Align the SiteMesh 3 feature title with SiteMesh 2 ("GSP SiteMesh 3
  Layouts") and clean up GspLayoutSpec (single quotes, drop the
  short-lived SNAPSHOT-repo assertions).
- Fix the UI glitch where replacing the default layout left both
  sitemesh3 and grails-layout selected. The two decorators are now
  driven by a single GspLayoutImpl option (SITEMESH3 default,
  GRAILS_LAYOUT) instead of a visible feature card, mirroring the
  servlet and reloading one-of groups. Both members are invisible
  DefaultFeatures that apply based on the selected option, so the
  default-features endpoint always resolves exactly one member.

Threads the option through Options, FeatureFilter, ApplicationController
and ContextFactory, exposes it via select-options (GspLayoutImplDTO /
GspLayoutImplSelectOptions) for the UI dropdown, and adds a --gsp-layout
CLI option. Updates BuildBuilder and GspLayoutSpec and adds
SelectOptionsControllerSpec.
@codeconsole codeconsole marked this pull request as draft June 5, 2026 20:06
The test apps already gate the layout dependency on the
SITEMESH3_TESTING_ENABLED environment variable, but no CI lane set it,
so the suite only ever exercised the legacy grails-layout. Set the flag
at the workflow level in the CI and Groovy joint builds so the example
apps and grails-test-suite-uber resolve grails-sitemesh3 by default.

No build-script changes needed: the flag stays as-is, the SiteMesh 2
anchors keep their grails-layout coverage, and developers can still drop
the flag to run against SiteMesh 2 locally.
@codeconsole codeconsole force-pushed the feat/sitemesh3-default-layout branch from f816246 to 021059d Compare June 5, 2026 23:47
@codeconsole

Copy link
Copy Markdown
Contributor Author

#15585

In the filterless SiteMesh 3 pipeline the GSP capture taglib writes the
full <head>/<body> markup to the response buffer and also captures the
head/body into a Sitemesh3CapturedPage. When no decorator is selected
(e.g. a view with no <meta name="layout"> and no matching convention or
default layout), SiteMeshView writes content.getData() back. The captured
page had neither renderedContent nor pageBuffer set, so getData()
reconstructed only from (empty) properties and emitted an empty
<html><head></head><body></body></html>.

Attach the original response buffer as the captured page's rendered
content in the no-merge branch so the original page is written back when
nothing decorates it. Decorated (meta-layout) pages are unaffected: they
take the decorate branch, which reads the head/body child properties.

Verified against grails-test-examples/app1 integration tests under
SITEMESH3_TESTING_ENABLED=true: ConfigTestControllerSpec,
ControllerIncludesSpec, ControllerFromPluginSpec and
ConditionalOnPropertyFromPluginYmlSpec now pass, with no regression to
meta-layout pages (BookFunctionalSpec).
The grails-fields plugin renders embedded objects by wrapping the
sub-fields in <g:applyLayout name="_fields/embedded" params="[...]">.
SiteMesh 3's applyLayout built the body Content from the request-scoped
Sitemesh3CapturedPage (the outer page being decorated) and ignored the
tag's params, so the _fields/embedded layout's <g:layoutBody/> rendered
empty and <g:pageProperty name="legend"/> / pageProperty(name:'type')
had no values. The result was an empty <fieldset class="embedded "> with
none of the address.* inputs. SiteMesh 2's GrailsLayoutTagLib pushed a
fresh layout page and copied params to page properties, which is why the
same grails-fields plugin worked under SiteMesh 2 but not SiteMesh 3.

applyLayout now pushes a fresh Sitemesh3CapturedPage for the body render
and restores the outer page in a finally block, wires the rendered body
in via setBodyBuffer so <g:layoutBody/> works, applies each params entry
as a page property for <g:pageProperty>, and emits the raw body verbatim
when no decorator is resolved (matching SiteMesh 2's no-decorator
fallback).

Verified under SITEMESH3_TESTING_ENABLED=true: scaffolding-fields
RelationshipsFunctionalSpec (embedded address fields) passes and the full
scaffolding-fields integration suite is green, with no regression to
meta-layout pages or the earlier undecorated-page fix.
database-cleanup, scaffolding-fields and test-phases declared
grails-layout directly in addition to grails-dependencies-starter-web.
starter-web already provides the layout plugin and flips it with
SITEMESH3_TESTING_ENABLED, so with the flag on these apps ended up with
both grails-sitemesh3 (via starter-web) and grails-layout (direct) on the
classpath, and failed to boot with a jspViewResolver bean-type clash.

Remove the redundant direct grails-layout dependency so each app gets
exactly one layout plugin from starter-web: SiteMesh 2 when the flag is
off, SiteMesh 3 when it is on. The dedicated SiteMesh 2 layout coverage
remains on gsp-layout, which does not use starter-web.
A controller's "render template: 'x'" renders the GSP directly with
renderView=false and must not be decorated with a layout. Under the
filterless SiteMesh 3 integration the template view was still wrapped by
GrailsSiteMeshView and Sitemesh3LayoutFinder picked the default layout,
so partials came back wrapped in the application layout (e.g. the main
layout's title instead of the partial's own). SiteMesh 2 suppressed this
via GrailsLayoutSelector keyed on the renderView flag.

Sitemesh3LayoutFinder.selectDecoratorPaths now returns no decorator when
the current render has renderView=false and no explicit layout was
requested. An explicit "render template: ..., layout: 'x'" still sets the
LAYOUT_ATTRIBUTE and is honoured, and normal view renders (renderView=true)
are unaffected, so decorated pages still decorate.

Verified under SITEMESH3_TESTING_ENABLED=true: LayoutWithTemplateSpec
passes; BookFunctionalSpec (meta-layout pages), ControllerIncludesSpec
(incl. include-from-template), ConfigTestControllerSpec and
scaffolding-fields RelationshipsFunctionalSpec remain green.
@codeconsole codeconsole marked this pull request as ready for review June 7, 2026 03:51

@matrei matrei left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a commented out grails-test-examples-gsp-sitemesh3 project.
I enabled it and ran the tests. They fail. Is this expected?

Comment thread .github/workflows/gradle.yml Outdated
@matrei

matrei commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

I think the Forge change actually made it worse, because now I cannot select grails-layout at all:

Screenshot from 2026-06-09 17-37-55

Invert the layout toggle so the default build resolves grails-sitemesh3;
set SITEMESH2_TESTING_ENABLED=true to test the legacy grails-layout
instead. Drop the now-redundant SITEMESH3_TESTING_ENABLED env from the
CI workflows since SiteMesh 3 is the default. SiteMesh 2 keeps coverage
through the hardcoded gsp-layout/jetty/scaffolding anchors.
The features endpoint already read gspLayout, but the preview, create
(zip), GitHub and diff endpoints built Options from explicit query params
and ignored it, so selecting SiteMesh 2 had no effect on the generated
project. Thread a gspLayout query option through those controllers and
their operation interfaces, mirroring servlet, so the generated build
resolves grails-layout when gspLayout=GRAILS_LAYOUT and grails-sitemesh3
by default.
Adding the gspLayout query option pushed GitHubCreateController.createApp
and its operation interface to 13 parameters, over the Checkstyle
ParameterNumber limit. The endpoint already receives a RequestInfo, which
carries the user agent, so the separate @Header user-agent parameter was
redundant; drop it and read requestInfo.getUserAgent() instead. This keeps
the method within the limit without suppressing the rule.
@codeconsole codeconsole force-pushed the feat/sitemesh3-default-layout branch from c0be007 to 852d132 Compare June 10, 2026 05:58
Use "if (" with a space and place "} else {" on a single line in the
SITEMESH2_TESTING_ENABLED toggle across the example and suite build files.
…ture

Per review, drop the GspLayoutImpl select option and its plumbing through
Options, the controllers and the CLI. The layout engine keeps the same
shape it has today, just inverted: SiteMesh 3 (grails-sitemesh3) is the
invisible default applied to web applications, and the "GSP SiteMesh 2
Layouts" feature (grails-layout) can be selected to override it. Because
the default is invisible it never appears as a feature card, which also
fixes the UI glitch where both layouts showed after selecting SiteMesh 2.
No grails-forge-ui changes are required.
@codeconsole codeconsole requested a review from matrei June 10, 2026 19:29
jamesfredley and others added 3 commits June 10, 2026 16:19
It was disabled when SiteMesh 3 lacked Spring Boot 4 support, which the
filterless integration has since restored. This is the dedicated SiteMesh 3
layout regression app, mirroring the gsp-layout SiteMesh 2 anchor.
Three gaps surfaced by re-enabling the gsp-sitemesh3 example, which tests
the layout engine itself rather than just using it:

- Nested <g:applyLayout> chains lost the inner layout's title, head and
  body wrappers: applyLayout overwrote the captured body with the full
  rendered document and passed the inner chain's decorated output on as
  raw text. Never overwrite a captured body buffer, and parse uncaptured
  full-document bodies through the content processor so chained
  decoration keeps head/title/body, as SiteMesh 2 does.

- Error-dispatched views (404/500 pages) rendered undecorated because
  Sitemesh3LayoutFinder guarded on the composite isRenderView(), which is
  false for any response status >= 300. Replace the guard with the
  SiteMesh 2 parity mechanism: a Sitemesh3RenderViewMutator registered as
  grailsRenderViewMutator that unwraps the SiteMesh view only for
  "render template:" partials, so error views decorate via their meta
  layout while partials stay undecorated.

- JSP views rendered empty on Tomcat 11 because JstlView forwards and
  ApplicationDispatcher suspends the wrapped response after the forward,
  discarding everything SiteMesh writes. Render InternalResourceView
  inner views via include instead, and honor the JSP's meta layout.

Verified: gsp-sitemesh3 integration suite 15/15, grails-sitemesh3 unit
tests 32/32 (new mutator/finder/resolver specs), no regressions in the
app1 layout/include/template specs or scaffolding-fields embedded fields.
@jamesfredley

Copy link
Copy Markdown
Contributor

I re-checked the current PR head (bb94c8c27d) against the earlier SM2 parity list. A few items look addressed now, but these still look partial or unresolved:

Item Status Notes
g:applyLayout attribute parity Partial RenderSitemeshTagLib.groovy:78 now handles body, name, and params (:112, :122), but still does not support the SM2 attribute surface: template, url, action, controller, model, contentType, encoding, or parse.
Default layout config + implicit application fallback Unresolved SM3 still reads only grails.sitemesh.default.layout (Sitemesh3GrailsPlugin.groovy:83) and passes null when unset (:102). It does not consult grails.views.layout.default, and does not match SM2's implicit application fallback behavior (GroovyPageLayoutFinder.groovy:180).
grails.views.layout.enable.nongsp Unresolved SM2 wires this through LayoutGrailsPlugin.groovy:38 and GroovyPageLayoutFinder.groovy:87. I do not see an SM3 equivalent in Sitemesh3LayoutFinder.java, which still only resolves via the GSP locator.
<title> stripping robustness Unresolved Sitemesh3CapturedPage.java:232 still searches for "<title" without checking the tag boundary, so cases like <titlebar> / <title-x> can still be misidentified before the real </title> is found.
Upstream auto-config suppression guard Partial There is improved view-resolver auto-config (Sitemesh3AutoConfiguration.java:38), but the plugin still relies on a disabled bean named sitemesh with NoopSitemeshFilter (Sitemesh3GrailsPlugin.groovy:112, :125). I do not see a guard test proving the upstream filter auto-config stays suppressed.
Test examples default path Partial Most examples now default to SM3 via SITEMESH2_TESTING_ENABLED, but the Hibernate 7 examples still use the old/inverted SITEMESH3_TESTING_ENABLED toggle and therefore default to SM2 when unset, e.g. grails-test-examples/hibernate7/grails-hibernate/build.gradle:41.
SM2-only examples without SM3 coverage Partial database-cleanup, scaffolding-fields, and test-phases were fixed, but scaffolding and jetty remain hardcoded to grails-layout only (scaffolding/build.gradle:49, jetty/build.gradle:41). gsp-layout can probably remain the intentional SM2 anchor.
CI coverage for SM2 lane Unresolved I do not see SITEMESH2_TESTING_ENABLED or SITEMESH3_TESTING_ENABLED in .github/workflows. The default SM3 path is exercised, but there is no explicit SM2 compatibility lane.
grails-gsp-spring-boot publication Unresolved The project is included in settings.gradle:175 and depends on grails-sitemesh3 (grails-gsp/spring-boot/build.gradle:37), but it is still absent from the published projects list in gradle/publish-root-config.gradle while grails-sitemesh3 is listed at :76.

The refreshed org.sitemesh 3.3.0-SNAPSHOT defaults the Boot starter to
the view-resolver integration (the servlet filter is now opt-in via
sitemesh.integration=filter, and the starter registers its own disabled
"sitemesh" guard bean), and SiteMeshViewResolver now switches
forward-based JSP inner views to include dispatch itself, keyed on
DispatchMode's container detection.

Remove the plugin pieces that existed to compensate:

- the NoopSitemeshFilter and its "sitemesh" FilterRegistrationBean,
  which suppressed the upstream filter auto-configuration
- Sitemesh3EnvironmentPostProcessor and its spring.factories/imports
  registrations, which forced sitemesh.integration and wrap-mode
  defaults; Sitemesh3AutoConfiguration now uses matchIfMissing instead
- the InternalResourceView alwaysInclude special case in
  GrailsSiteMeshViewResolver, now handled upstream before the
  createSiteMeshView hook; the resolver spec stubs Tomcat 11 server
  info to verify the inherited behavior

Verified against the updated snapshot: grails-sitemesh3 unit tests and
codeStyle, gsp-sitemesh3 integration suite 15/15 (including the JSP
demo on Tomcat 11), app1 layout/include/template specs and
scaffolding-fields embedded fields all green.
The hibernate7 examples were cloned from the hibernate5 tree before the
SiteMesh toggle was inverted, so they still gated on the removed
SITEMESH3_TESTING_ENABLED variable and silently defaulted to SiteMesh 2.
Bring them in line with the other examples: grails-sitemesh3 by default,
grails-layout via SITEMESH2_TESTING_ENABLED.
Sitemesh3CapturedPage.extractHead matched "<title" as a bare prefix, so
an element such as <titlebar> or <title-x> appearing before the real
title made the strip start at the wrong tag and delete everything up to
the real </title>, silently dropping head content. Require the character
after the prefix to terminate the tag name ('>' or whitespace) and keep
scanning otherwise. Covered by a new Sitemesh3CapturedPageSpec.
Sitemesh3LayoutFinder only consulted the configured default layout and
returned nothing when unset, while SiteMesh 2's GroovyPageLayoutFinder
falls back to the implicit "application" layout — so apps relying on
layouts/application.gsp silently lost decoration when switching. The
finder now mirrors SiteMesh 2 exactly: the configured default is used
as-is, and only when none is configured is the implicit application
layout tried (a configured-but-missing default does not additionally
fall back). The plugin also honors SiteMesh 2's
grails.views.layout.default config key when the SiteMesh 3 specific
grails.sitemesh.default.layout is unset.
Both were hardcoded to grails-layout as SiteMesh 2 anchors. With the
gsp-layout example serving as the dedicated SiteMesh 2 regression
anchor, switch them to the standard SITEMESH2_TESTING_ENABLED toggle so
the default build exercises SiteMesh 3 — notably adding the only
Jetty-with-SiteMesh-3 integration coverage in the repository.
The module exists to let plain Spring Boot applications use GSP (with
SiteMesh 3 layouts and JSP taglib support), so it must ship: add it to
the published projects list. Re-enable the gsp-spring-boot test example
so the standalone Boot + GSP combination is at least compiled and
GSP-precompiled by the default build; the example currently carries no
tests, so functional coverage remains a follow-up.

Verified: grails-gsp-spring-boot publishToMavenLocal produces the
artifact, and the re-enabled example builds cleanly.
SiteMesh 3's g:applyLayout only handled name, params and the tag body;
the SiteMesh 2 implementation also selects the content to decorate via
template (with model and the other g:render attributes), url, and
action/controller (delegating to g:include with params), plus parse to
force a fresh head/title/body parse and contentType for the SiteMesh
context. Mirror those semantics: each content source produces a buffer
that flows through the existing captured-page pipeline (fresh page push
and restore, capture reuse or content-processor parse, params as page
properties, raw emission when no decorator resolves), and the model now
reaches the layout render through GrailsSiteMeshViewContext.

Deliberate differences from the SiteMesh 2 code, not its docs: encoding
is accepted but unused (SiteMesh 2 never read it either), url content is
always parsed (as in SiteMesh 2, where no captured page exists for that
path), and contentType cannot switch parsers since SiteMesh 3 uses a
single configured ContentProcessor. The fresh captured page is pushed
for url/include renders too, preserving the nested-render isolation.

Adds six gsp-sitemesh3 end-to-end cases (template, model, url,
action/controller, parse, no-decorator), GrailsSiteMeshViewContext model
unit cases, and documents the previously missing action, controller and
parse attributes in the applyLayout tag reference.
@testlens-app

testlens-app Bot commented Jun 11, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 536da4d
▶️ Tests: 20505 executed
⚪️ Checks: 43/43 completed


Learn more about TestLens at testlens.app.

@codeconsole codeconsole changed the title Make SiteMesh 3 the default GSP layout 8.x Make SiteMesh 3 the default GSP layout Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants