Previous | Index

Shell Project Structure and Asset Load Chain

This page explains where app assets live, how ShellBuilder composes output, and what framework behavior is automatic.

Project Structure for Assets

Framework assets:

  • simplypages/src/main/resources/static/css/framework.css
  • simplypages/src/main/resources/static/js/framework.js

Typical application assets (Spring Boot):

  • src/main/resources/static/css/app.css
  • src/main/resources/static/css/pages/<page>.css
  • src/main/resources/static/css/modules/<module>.css
  • src/main/resources/static/js/app.js

ShellBuilder Load Chain

ShellBuilder.build() generates a full HTML document (<!DOCTYPE html>, <html>, <head>, <body>).

ShellBuilder.buildTemplate() generates a reusable compiled shell template with a dedicated content slot.

Head assets are added in this order:

  1. framework CSS (if enabled)
  2. custom CSS files in configured order
  3. HTMX script (if enabled)
  4. framework JS (/js/framework.js)
  5. custom JS files in configured order

Body behavior:

  • top banner and top nav (if configured; withAccountBar(...) remains compatibility alias)
  • mobile sidebar toggle (when sidebar is configured)
  • shell layout container and content target
  • optional inline HTMX nav-active script (when HTMX is enabled)

Desktop collapse and mobile drawer initialization live in the shared framework JS, not a generated inline collapse script. All head scripts retain defer; custom assets keep their configured order.

Both full and compiled-template heads include a private compatibility marker only when the builder includes HTMX. The top of the existing framework script uses it to initialize selfRequestsOnly: false, methodsThatUseUrlParams: ['get'], and scrollBehavior: 'smooth' for the bundled HTMX 2.0.11. This preserves the legacy request-origin, DELETE-body, and HTMX default-scroll behavior before custom JS executes. The marker is not a supported public configuration knob and loads no additional asset.

Immediate deferred custom-script assignments and custom DOMContentLoaded handlers can override these values. Explicit upstream htmx-config meta is merged later by HTMX and also wins. The framework does not reapply defaults during insertion or settle. Do not replace this ordering with generated standard compatibility meta: its later merge would overwrite immediate custom-script choices. withHtmx(false) emits no HTMX asset or private marker; framework JS alone leaves the host's defaults alone.

Body-Only Fragments

ShellBuilder.buildBody() returns a .shell-body fragment with the optional sidebar/mobile toggle and the main content target. It omits the document (doctype, html, head, body), header, banner, top navigation, stylesheet/script tags, and inline scripts. Content targeting, wrapping, and the default HTMX content-load attributes still apply; template mode instead uses its explicit slot.

The fragment contains no compatibility marker. The host document loads framework CSS and JS once, plus HTMX when used. Do not reload assets with each fragment. Unmarked/manual hosts explicitly choose HTMX policy through upstream configuration; loading framework JS does not select legacy defaults for them. See the asset reference for manual-host settings and the bounded Form-source header handler. Compatibility does not enforce allowed destinations, CORS, CSRF, or authorization. Shared framework JS initializes both existing shells and later direct/HTMX-inserted fragments idempotently. The collapsible desktop button remains Toggle sidebar, with the legacy toggleSidebar() activation and sidebarCollapsed storage key. Persistence is best effort; when storage is denied, toggling and later fragment initialization retain document-local preference. The mobile drawer remains separate from desktop collapse and from a Page's native disclosure.

CSS Loading Modes

Default mode:

ShellBuilder.create().build();

Replace framework CSS path:

ShellBuilder.create()
    .withFrameworkCssPath("/css/base.css")
    .build();

Framework + multiple custom stylesheets:

ShellBuilder.create()
    .withCustomCss("/css/app.css")
    .addCustomCss("/css/pages/dashboard.css")
    .addCustomCss("/css/modules/metrics.css")
    .build();

Framework JS + custom page scripts:

ShellBuilder.create()
    .withCustomJs("/js/app.js")
    .addCustomJs("/js/pages/dashboard.js")
    .build();

Replace all custom JS paths with an explicit ordered list:

ShellBuilder.create()
    .withCustomJs(List.of("/js/vendor/charts.js", "/js/pages/dashboard.js"))
    .build();

No framework CSS (advanced):

ShellBuilder.create()
    .withFrameworkCss(false)
    .withCustomCss("/css/app.css")
    .build();

Where to Put Your Own CSS

Use this layering pattern:

  1. app-wide theme and shared selectors in app.css
  2. page selectors in css/pages/...
  3. module selectors in css/modules/...

Keep selectors anchored to stable IDs/classes you set in Java (withId, withClass).

Behind-the-Scenes Behavior

ShellBuilder automatically handles:

  • inclusion of framework CSS unless disabled
  • inclusion of framework JS
  • optional HTMX bootstrap script inclusion
  • default content target ID (content-area) and optional HTMX load trigger (hx-get="/home" when HTMX is enabled)
  • optional content target hooks (withContentTargetId, withContentTargetClass, withContentWrapper)

Template mode behavior (buildTemplate()):

  • uses explicit content-slot rendering for the content target
  • treats withContent(...) as default slot content
  • does not emit shell auto-load attributes (hx-get="/home", hx-trigger="load") on the content target

Nothing else is auto-loaded for you. Any additional app CSS/JS must be linked explicitly by your app shell strategy.

Integration Guidance

  • Use framework defaults + custom CSS for fastest onboarding.
  • Use framework replacement or no-framework mode only if your team owns full design system styling.
  • If you disable framework CSS without replacement styles, built-in components will likely render unstyled.

Mobile Behavior Placement

  • Markup hooks come from shell/layout builders.
  • Responsive behavior is in framework.css.
  • Interactive sidebar open/close behavior is in framework.js.

Custom script order remains deterministic: HTMX (optional) -> framework JS -> custom JS in the order configured through withCustomJs(...) / addCustomJs(...).

For a full mobile behavior map, read:

  • docs/core/07-mobile-rendering-model-and-responsive-behavior.md