Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
c975l-site-layout
c975l/site-bundle
claude-code
symfony
bundle
website

Use this skill when working on the shell of a page in a Symfony application built on the c975L ecosystem with c975l/site-bundle — the layout, its Twig blocks, the theme tokens, the error pages, the email layouts or the footer components. Covers what a template must set, which block to override, where a design token belongs and what a Content-Security-Policy nonce forbids. Triggers on: layout.html.twig, bodyClass, heading, summarySocialNetwork, theme, themes/site.css, ScaffoldThemeTest, flashes, Scroll:Buttons, backTop, pullDown, --navbar-height, --reading-max-width, --title-color, --label-color, --primary-ink, --bottom-bar-height, error404, emails/fullLayout, emailUnsubscribe, EmailLayoutProvider, EmailLayoutTemplateProvider, layout_no_spam, layout_hello, layout_closing, layout_sent_by, email_template_body, site-owner, url-privacy-policy, HostedBy, MadeBy, Preconnect, theme_variables_css, absolute_urls, ui.management_stylesheet, getManagementStylesheets, block-thumbs, alternates, hreflang, page_alternates, page_title, page_summary.

Install
php artisan boost:add-skill c975l/site-bundle

Save this content to: .claude/skills/c975l-site-layout/SKILL.md

# c975L SiteBundle — layout, theme and emails

> The shell every page of a c975L site renders into: the base layout and its Twig blocks, the admin-editable theme, the error templates and the branded email layouts.

**Package:** `c975l/site-bundle` · **Namespace:** `c975L\SiteBundle\` · **Twig namespace:** `@c975LSite` · **Translation domain:** `site`

**Key source paths** (relative to the package root):
`templates/layout.html.twig`, `templates/components/General/`, `templates/Exception/`, `templates/emails/`, `src/Service/EmailLayoutProvider.php`, `sass/`, `scaffold/assets/styles/themes/site.css`, `config/configs.json`

**Related skills:** `c975l-site-pages`, `c975l-site-menus`, `c975l-site-seo`, `c975l-site-assets` in this same package. The theme compiler, the block system and the fonts are in `c975l/core-bundle`.

## The layout contract

`c975l:scaffold:install` puts `templates/layout.html.twig` in the app — shipped by ConfigBundle, so the
scaffolded login pages have a shell whether or not this bundle is installed:

```twig
{% extends ['@c975LSite/layout.html.twig', '@c975LUi/layout.html.twig'] %}
```

Twig takes the first template of the list that exists: full layout with this bundle, minimal shell
without. **The file is the app's from then on.** Replacing it with your own markup makes it the parent
of the database pages too (`@c975LSite/pages/page.html.twig` extends `layout.html.twig`), so it must
hold up that end of the contract: define a `container` block, and read the `title` and `robots`
variables. A replacement defining neither renders every page empty, with no error at all.

**This bundle's layout is a child of core-bundle's, not a copy of it.** `@c975LUi/layout.html.twig` is
the single source for the whole `<head>`, the body skeleton and the SEO cascade; `@c975LSite/layout.html.twig`
extends it and adds only what having Pages, menus and a navbar brings — four block overrides (`header`,
`container`, `footer`, `navigation`) and four variables handed over to the parent before it renders:

```twig
{% set ogImageMedia = page.ogImage %}      {# opens the parent's share-image cascade #}
{% set headingDisplayed = page.isTitleDisplayed %}
{% set bodyClasses = bodyClasses|default('') ~ 'navbar-fixed ' %}   {# appended, never assigned #}
{% set bodyControllers = (bodyControllers|default('') ~ ' basic')|trim %}
```

A child layout may set variables outside a block — Twig compiles its body before the parent's — but
never output. The two body variables are **appended, never assigned**: an app layout extending this one
runs its own `set` first, and assigning would drop what it declared. **Anything concerning the `<head>`
belongs to core-bundle's layout**, where both shells read it.

**A satellite bundle never ships a page layout.** Its templates extend `layout.html.twig` by name, no
`@` prefix, and the app's file resolves it.

## What a page template sets

```twig
{% set title = 'My page title' %}
{% set summarySocialNetwork = 'A short summary for social networks.' %}
```

`summarySocialNetwork` feeds both `<meta name="description">` and `og:description`. A page setting
neither can state them from the back office instead, through the `UrlMetadata` row CoreBundle holds for
its url — the layout only reads that row for what the template left unsaid.

An `alternates` variable — `hreflang => absolute url` — is what the layout writes its
`<link rel="alternate" hreflang>` tags from, and nothing is emitted when it is empty. A template
rendering a database page sets it from `page_alternates(page)`, and reads the two texts above through
`page_title(page)` / `page_summary(page)` so they come out in the language being served (see
`c975l-site-pages` and `c975l-site-seo`).

## Twig blocks of the layout

`head`, `meta`, `title`, `stylesheets`, `preconnect`, `fontPreload`, `body`, `bodyClass`, `header`,
`navigation`, `main`, `heading`, `flashes`, `container`, `content`, `share`, `navigationBottom`,
`footer`, `javascripts`, `importmap` — all of them core-bundle's except `navigation`.

`title` is the `<title>` tag and `heading` the `<h1>` printed above the content. **A template
suppressing its own heading overrides `heading`, never `title`** — the second empties the browser tab
and every share instead.

```twig
{% block share %}{{ parent() }}{# added content #}{% endblock %}
{% block share %}{% endblock %}   {# disabled #}
```

`flashes` is the one block carrying a guard, and the guard is **inside** it: the bag is only read for a
visitor carrying the session cookie, or for a request that started the session itself. Reading
`app.flashes` is enough to open a session — and a connection to its store — so an unguarded read costs
one on every anonymous page and every crawler hit, for flashes that cannot exist. **An override takes
the guard over with the block**: filled with something else than flashes it renders for everyone, and
filled with another read of `app.flashes` it has to ask `ui_can_hold_flash()` itself. Core-bundle owns
that function and its test suite locks the condition.

Only `success`, `info`, `warning` and `danger` carry a tint. The block maps the labels other bundles
emit onto them — `error` onto `danger`, `notice` onto `info` — and any other label onto `info`, an
untinted `alert-*` printing black ink on the dark page's own background.

Overriding `header` with more than the navigation inside drops the sticky navbar the stylesheet
arranges through that `<header>` (see `c975l-site-menus`).

`<body>` is a flex column at least as tall as the viewport, `<main>` taking the free space, so the
footer holds the bottom of a short page. A site-wide band contributed by another bundle renders
outside `<main>`, pulled in by core-bundle's layout with an include that keeps that bundle optional:

```twig
{{ include('@c975LSocial/shareButtons/default.html.twig', ignore_missing: true) }}
```

A direct call to that bundle's Twig function would resolve at compile time and break the layout of a
site not installing it. **Any bundle contributing a site-wide band follows this convention, its
template path being the public contract.**

## Content-Security-Policy

Core-bundle's layout asks for a `style` nonce as well as a `script` one, which makes `'unsafe-inline'` inert for
`style-src` on every public page: **a `style=""` attribute in one of your templates is dropped by the
browser.** Move those declarations to a class. Styles written from JavaScript are unaffected, the CSSOM
not being subject to `style-src`. If `nelmio/security-bundle` is installed, `CookieNonceGenerator`
decorates its nonce generator to keep the nonce stable across a visit, so Turbo re-executing a fetched
page's `<script>` is not blocked.

## Theme

Colors, fonts and light/dark mode are **admin-editable config keys declared by UiBundle** (`theme-color-*`,
`theme-font-family-*`, `theme-mode`), compiled into `--c975l-*` custom properties. Do not restate them
in CSS.

What a design owns is the shapes, in `assets/styles/themes/*.css`, copied into the app by
`c975l:scaffold:install` and owned by it from then on. **One file per bundle, named after it** —
`ui.css` for everything the block layer reads, `site.css` for this site's chrome (navbar, footer,
`--scroll-offset`, `--frame-background`), one more per satellite that reads tokens of its own. Every
token ships commented out at its default: uncomment a line to take it over, leave it and it keeps
following the bundle.

Tokens worth knowing, all in `site.css`:

| Token | Default | Retunes |
| --- | --- | --- |
| `--navbar-padding-y` / `--navbar-content-height` | `8px` / `44px` | the bar's height; `--navbar-height` is the two together |
| `--navbar-width` / `--navbar-margin-x`, `--footer-width` / `--footer-margin-x` | full viewport | whether the bars bleed past `--body-max-width` |
| `--footer-margin-top` | `--section-space` | the gap above the footer band |
| `--font-size-body` | `1rem` | the size copy is read at; headings are sized against the root and stay out |
| `--title-color` | `var(--primary-ink)` | the ink of every heading, without repainting the brand |
| `--link-color` / `--link-hover-color` | `var(--primary-ink)` / `var(--link-color)` | a link at rest, and on hover |
| `--reading-max-width` | `min(75ch, 90vw)` | the measure body copy is laid out on |
| `--footer-items-direction` / `--footer-items-justify` | column | the footer's layout, unless an admin picks a Display style |
| `--footer-grid-columns` / `--footer-grid-rows` | `1.6fr repeat(3, 1fr)` / `auto auto` | the tracks of the *Columns* display style, read from 768px up |

**Brand-colored text reads `--primary-ink`, never `--primary`.** UiBundle's token splits the brand's
two roles: `--primary` is the **fill** a button, a `primary` flat and the footer band need dark, while
`--primary-ink` is the same color read **against** the page — a heading, a link, an outline, a rule,
a focus ring. It defaults to `--primary`, so a light page is unchanged; `sass/_theme-dark.scss`
lightens that one token and every ink follows, a deep brand blue reading at 1.43:1 on the `#121212`
page. `DarkThemeTextTokensTest` locks it, and `PrimaryInkRoleTest` scans `sass/` for any ink property
still reading `--primary`.

**Declare a token on `:root, [data-theme]`, never on `:root` alone.** A `var()` inside a custom
property's value is substituted on the element carrying the declaration: on `:root` only, every derived
token is computed once against the root palette and descends already resolved, so a scope opened
further down cannot repaint it. `ThemeScopeTest` locks the selector.

Stay out of the theme file: colors and fonts (the admin's), the per-variant section tokens UiBundle
mixes inside its own `.section--bg-*` rules (declaring them in `:root` collapses the three variants),
and the tokens JavaScript writes at runtime. `--bottom-bar-height` stays out too, for a reason of its own: a
bundle fixing a bar to the bottom of the viewport (ShopBundle's basket bar) declares it on `<body>` with that
bar's height, and UiBundle's scroll buttons step over it by reading it — a value in `:root` would raise them on
every site, bar or not.

`App\Service\ThemeStylesheetProvider`, also scaffolded, contributes the whole `themes/` directory to
UiBundle's stylesheet registry, so the files are concatenated into the single `bundles/build/site.css`
rather than costing one request each. **Never import a stylesheet from `assets/app.js`**: AssetMapper
addresses a CSS entry by a `data:application/javascript,` URL, which the site's own CSP blocks, taking
the whole entrypoint down with it.

Two registries, not one: `ui.stylesheet` builds the sheet the public pages link, `ui.management_stylesheet`
the one the back office links. This bundle's `StylesheetProvider` feeds both — its `styles.min.css` to the
first, and `block-thumbs.min.css`, the silhouettes of its block kinds shown by the visual block picker, to
the second. A sheet only the admin screens need belongs in the second alone, rather than on every page of
every site.

## Error templates

`error`, `error401`, `error403`, `error404`, `error410`, `error500` under `@c975LSite/Exception/`:

```twig
{% extends 'layout.html.twig' %}
{% block content %}{% include '@c975LSite/Exception/error404.html.twig' %}{% endblock %}
{% block share %}{% endblock %}
```

The layout defaults `robots` to `noindex, follow` on any page carrying a `status_code`, so no error
template can forget it.

## Emails

Templates under `@c975LSite/emails/`: `layout.html.twig`, `fullLayout.html.twig`, `header.html.twig`
and `footer.html.twig` (which render the `email-header` / `email-footer` menus), and
`emailTemplateLayout.html.twig`. CSS is inlined by `twig/cssinliner-extra`, the compiled theme being
appended through `theme_variables_css()` so the admin's colors win the cascade.

`fullLayout` wraps the whole document in `absolute_urls(config('site-url'))` (core-bundle's filter,
`^1.17.5`), so every root-relative `src` and `href` an email holds names the host it hangs from — a
mailbox has nothing to resolve `/medias/…` against. It is the one place to do it, since every other
email template extends this one; an unset `site-url` leaves the paths untouched.

`fullLayout`'s own copy is four `EmailTemplate` rows — `layout_no_spam`, `layout_hello`,
`layout_closing`, `layout_sent_by` — declared by `EmailLayoutTemplateProvider` and seeded by
`c975l:ui:email-templates:ensure`, **one row per declared language**, read through
`email_template_body('layout_hello', {}, locale)`. Each is an `html` block, so a line break or a bold
word survives the send. The last two take a `{{ site }}` placeholder. They were the `email-text-*`
config entries until 02/09/2026.

What the footer owes the law is not that copy: the identification comes from `site-owner`, followed by
a link to the page `url-privacy-policy` names, both skipped while their setting is empty, and both
rendered *below* the `email-footer` menu an admin builds. `{% block emailUnsubscribe %}` is where a
prospection email places its own way out (article L34-5 CPCE); a transactional one leaves it empty.

`EmailLayoutProvider` implements UiBundle's `EmailLayoutProviderInterface`, so any `EmailTemplate` sent
anywhere in the ecosystem comes out in this bundle's branded layout — and in UiBundle's plain fallback
on a site not installing it. `wrap(string $bodyHtml, ?string $locale = null)` receives the language the
body was resolved in and hands it to the template, which passes it to each `email_template_body()` call:
without it the four sentences followed whichever row the database returned first. **Do not write an email layout in a satellite bundle**; send with
`wrapLayout: true` and this provider answers.

## Footer components

```twig
<twig:c975LSite:General:HostedBy/>
<twig:c975LSite:General:MadeBy/>
```

Both read `site-hosted-by-*` / `site-made-by-*` from the config, and what they show comes from
`display-hosted-by` / `display-made-by` (`none`, `logo`, `name`, `logo-name`) through ConfigBundle's
`credits_mode()`. `MadeBy` words its label through `made_by_label()`, reading `made-by-wording`
(`made` → "Réalisé par", `powered` → "Propulsé par" for a site only running the system).
`site-preconnect` is a JSON array of external origins to preconnect to, read by core-bundle's layout,
which adds the Matomo origin to it on its own.

Matomo, the cookie banner and the scroll buttons **belong to UiBundle, and its layout renders them** —
`<twig:c975LUi:Analytics:Matomo/>`, `<twig:c975LUi:Cookie:Consent/>` and
`<twig:c975LUi:Scroll:Buttons/>`. Rendered by the layout and not by the footer, so **a site overriding
`footer` keeps its tracking and its cookie banner**. The first two carry their own enable guard; the
buttons point at the `id="top"` set on `<body>` and at the `<span id="bottom">` closing it, so a layout
of your own keeps both anchors.

## Do not

- **Do not ship a page layout from a satellite bundle**, and do not `{% extends '@c975LSite/…' %}`
  with the `@` prefix from one — extend `layout.html.twig` by name.
- **Do not replace the scaffolded layout without defining `container` and reading `title`/`robots`** —
  the database pages render empty, silently.
- **Do not write a `style=""` attribute** in a template. The nonce makes it inert.
- **Do not read `app.flashes` outside the layout's guard.** The read opens a session for every visitor.
- **Do not import a stylesheet from `assets/app.js`.** CSP blocks the whole entrypoint.
- **Do not declare a theme token on `:root` alone.** Use `:root, [data-theme]`.
- **Do not set colors or fonts in a theme file** — they belong to the admin, in the config screen.
- **Do not paint text, an outline or a rule with `var(--primary)`.** Read `var(--primary-ink)`, which
  is what dark mode lightens; `--primary` is the fill a surface needs.
- **Do not call another bundle's site-wide band function from the layout.** Include its template with
  `ignore_missing`.
- **Do not write a `<head>` tag in this bundle's layout.** Core-bundle's is the only one, and a copy
  here drifts from it without a word.
- **Do not override `title` to hide a page heading** — it is the `<title>` tag. Override `heading`.
- **Do not hardcode the email copy.** The four `layout_*` templates are the site's own, one row per language.
- **Do not make an email's paths absolute template by template.** `fullLayout` already does it for all.
- **Do not read a `color-mix()` token in an email stylesheet.** No mail client resolves it and the
  inliner drops the declaration — state the value, as `sass/_email-footer.scss` does for `--label-color`.

c975L SiteBundle — layout, theme and emails

The shell every page of a c975L site renders into: the base layout and its Twig blocks, the admin-editable theme, the error templates and the branded email layouts.

Package: c975l/site-bundle · Namespace: c975L\SiteBundle\ · Twig namespace: @c975LSite · Translation domain: site

Key source paths (relative to the package root): templates/layout.html.twig, templates/components/General/, templates/Exception/, templates/emails/, src/Service/EmailLayoutProvider.php, sass/, scaffold/assets/styles/themes/site.css, config/configs.json

Related skills: c975l-site-pages, c975l-site-menus, c975l-site-seo, c975l-site-assets in this same package. The theme compiler, the block system and the fonts are in c975l/core-bundle.

The layout contract

c975l:scaffold:install puts templates/layout.html.twig in the app — shipped by ConfigBundle, so the scaffolded login pages have a shell whether or not this bundle is installed:

{% extends ['@c975LSite/layout.html.twig', '@c975LUi/layout.html.twig'] %}

Twig takes the first template of the list that exists: full layout with this bundle, minimal shell without. The file is the app's from then on. Replacing it with your own markup makes it the parent of the database pages too (@c975LSite/pages/page.html.twig extends layout.html.twig), so it must hold up that end of the contract: define a container block, and read the title and robots variables. A replacement defining neither renders every page empty, with no error at all.

This bundle's layout is a child of core-bundle's, not a copy of it. @c975LUi/layout.html.twig is the single source for the whole <head>, the body skeleton and the SEO cascade; @c975LSite/layout.html.twig extends it and adds only what having Pages, menus and a navbar brings — four block overrides (header, container, footer, navigation) and four variables handed over to the parent before it renders:

{% set ogImageMedia = page.ogImage %}      {# opens the parent's share-image cascade #}
{% set headingDisplayed = page.isTitleDisplayed %}
{% set bodyClasses = bodyClasses|default('') ~ 'navbar-fixed ' %}   {# appended, never assigned #}
{% set bodyControllers = (bodyControllers|default('') ~ ' basic')|trim %}

A child layout may set variables outside a block — Twig compiles its body before the parent's — but never output. The two body variables are appended, never assigned: an app layout extending this one runs its own set first, and assigning would drop what it declared. Anything concerning the <head> belongs to core-bundle's layout, where both shells read it.

A satellite bundle never ships a page layout. Its templates extend layout.html.twig by name, no @ prefix, and the app's file resolves it.

What a page template sets

{% set title = 'My page title' %}
{% set summarySocialNetwork = 'A short summary for social networks.' %}

summarySocialNetwork feeds both <meta name="description"> and og:description. A page setting neither can state them from the back office instead, through the UrlMetadata row CoreBundle holds for its url — the layout only reads that row for what the template left unsaid.

An alternates variable — hreflang => absolute url — is what the layout writes its <link rel="alternate" hreflang> tags from, and nothing is emitted when it is empty. A template rendering a database page sets it from page_alternates(page), and reads the two texts above through page_title(page) / page_summary(page) so they come out in the language being served (see c975l-site-pages and c975l-site-seo).

Twig blocks of the layout

head, meta, title, stylesheets, preconnect, fontPreload, body, bodyClass, header, navigation, main, heading, flashes, container, content, share, navigationBottom, footer, javascripts, importmap — all of them core-bundle's except navigation.

title is the <title> tag and heading the <h1> printed above the content. A template suppressing its own heading overrides heading, never title — the second empties the browser tab and every share instead.

{% block share %}{{ parent() }}{# added content #}{% endblock %}
{% block share %}{% endblock %}   {# disabled #}

flashes is the one block carrying a guard, and the guard is inside it: the bag is only read for a visitor carrying the session cookie, or for a request that started the session itself. Reading app.flashes is enough to open a session — and a connection to its store — so an unguarded read costs one on every anonymous page and every crawler hit, for flashes that cannot exist. An override takes the guard over with the block: filled with something else than flashes it renders for everyone, and filled with another read of app.flashes it has to ask ui_can_hold_flash() itself. Core-bundle owns that function and its test suite locks the condition.

Only success, info, warning and danger carry a tint. The block maps the labels other bundles emit onto them — error onto danger, notice onto info — and any other label onto info, an untinted alert-* printing black ink on the dark page's own background.

Overriding header with more than the navigation inside drops the sticky navbar the stylesheet arranges through that <header> (see c975l-site-menus).

<body> is a flex column at least as tall as the viewport, <main> taking the free space, so the footer holds the bottom of a short page. A site-wide band contributed by another bundle renders outside <main>, pulled in by core-bundle's layout with an include that keeps that bundle optional:

{{ include('@c975LSocial/shareButtons/default.html.twig', ignore_missing: true) }}

A direct call to that bundle's Twig function would resolve at compile time and break the layout of a site not installing it. Any bundle contributing a site-wide band follows this convention, its template path being the public contract.

Content-Security-Policy

Core-bundle's layout asks for a style nonce as well as a script one, which makes 'unsafe-inline' inert for style-src on every public page: a style="" attribute in one of your templates is dropped by the browser. Move those declarations to a class. Styles written from JavaScript are unaffected, the CSSOM not being subject to style-src. If nelmio/security-bundle is installed, CookieNonceGenerator decorates its nonce generator to keep the nonce stable across a visit, so Turbo re-executing a fetched page's <script> is not blocked.

Theme

Colors, fonts and light/dark mode are admin-editable config keys declared by UiBundle (theme-color-*, theme-font-family-*, theme-mode), compiled into --c975l-* custom properties. Do not restate them in CSS.

What a design owns is the shapes, in assets/styles/themes/*.css, copied into the app by c975l:scaffold:install and owned by it from then on. One file per bundle, named after it — ui.css for everything the block layer reads, site.css for this site's chrome (navbar, footer, --scroll-offset, --frame-background), one more per satellite that reads tokens of its own. Every token ships commented out at its default: uncomment a line to take it over, leave it and it keeps following the bundle.

Tokens worth knowing, all in site.css:

Token Default Retunes
--navbar-padding-y / --navbar-content-height 8px / 44px the bar's height; --navbar-height is the two together
--navbar-width / --navbar-margin-x, --footer-width / --footer-margin-x full viewport whether the bars bleed past --body-max-width
--footer-margin-top --section-space the gap above the footer band
--font-size-body 1rem the size copy is read at; headings are sized against the root and stay out
--title-color var(--primary-ink) the ink of every heading, without repainting the brand
--link-color / --link-hover-color var(--primary-ink) / var(--link-color) a link at rest, and on hover
--reading-max-width min(75ch, 90vw) the measure body copy is laid out on
--footer-items-direction / --footer-items-justify column the footer's layout, unless an admin picks a Display style
--footer-grid-columns / --footer-grid-rows 1.6fr repeat(3, 1fr) / auto auto the tracks of the Columns display style, read from 768px up

Brand-colored text reads --primary-ink, never --primary. UiBundle's token splits the brand's two roles: --primary is the fill a button, a primary flat and the footer band need dark, while --primary-ink is the same color read against the page — a heading, a link, an outline, a rule, a focus ring. It defaults to --primary, so a light page is unchanged; sass/_theme-dark.scss lightens that one token and every ink follows, a deep brand blue reading at 1.43:1 on the #121212 page. DarkThemeTextTokensTest locks it, and PrimaryInkRoleTest scans sass/ for any ink property still reading --primary.

Declare a token on :root, [data-theme], never on :root alone. A var() inside a custom property's value is substituted on the element carrying the declaration: on :root only, every derived token is computed once against the root palette and descends already resolved, so a scope opened further down cannot repaint it. ThemeScopeTest locks the selector.

Stay out of the theme file: colors and fonts (the admin's), the per-variant section tokens UiBundle mixes inside its own .section--bg-* rules (declaring them in :root collapses the three variants), and the tokens JavaScript writes at runtime. --bottom-bar-height stays out too, for a reason of its own: a bundle fixing a bar to the bottom of the viewport (ShopBundle's basket bar) declares it on <body> with that bar's height, and UiBundle's scroll buttons step over it by reading it — a value in :root would raise them on every site, bar or not.

App\Service\ThemeStylesheetProvider, also scaffolded, contributes the whole themes/ directory to UiBundle's stylesheet registry, so the files are concatenated into the single bundles/build/site.css rather than costing one request each. Never import a stylesheet from assets/app.js: AssetMapper addresses a CSS entry by a data:application/javascript, URL, which the site's own CSP blocks, taking the whole entrypoint down with it.

Two registries, not one: ui.stylesheet builds the sheet the public pages link, ui.management_stylesheet the one the back office links. This bundle's StylesheetProvider feeds both — its styles.min.css to the first, and block-thumbs.min.css, the silhouettes of its block kinds shown by the visual block picker, to the second. A sheet only the admin screens need belongs in the second alone, rather than on every page of every site.

Error templates

error, error401, error403, error404, error410, error500 under @c975LSite/Exception/:

{% extends 'layout.html.twig' %}
{% block content %}{% include '@c975LSite/Exception/error404.html.twig' %}{% endblock %}
{% block share %}{% endblock %}

The layout defaults robots to noindex, follow on any page carrying a status_code, so no error template can forget it.

Emails

Templates under @c975LSite/emails/: layout.html.twig, fullLayout.html.twig, header.html.twig and footer.html.twig (which render the email-header / email-footer menus), and emailTemplateLayout.html.twig. CSS is inlined by twig/cssinliner-extra, the compiled theme being appended through theme_variables_css() so the admin's colors win the cascade.

fullLayout wraps the whole document in absolute_urls(config('site-url')) (core-bundle's filter, ^1.17.5), so every root-relative src and href an email holds names the host it hangs from — a mailbox has nothing to resolve /medias/… against. It is the one place to do it, since every other email template extends this one; an unset site-url leaves the paths untouched.

fullLayout's own copy is four EmailTemplate rows — layout_no_spam, layout_hello, layout_closing, layout_sent_by — declared by EmailLayoutTemplateProvider and seeded by c975l:ui:email-templates:ensure, one row per declared language, read through email_template_body('layout_hello', {}, locale). Each is an html block, so a line break or a bold word survives the send. The last two take a {{ site }} placeholder. They were the email-text-* config entries until 02/09/2026.

What the footer owes the law is not that copy: the identification comes from site-owner, followed by a link to the page url-privacy-policy names, both skipped while their setting is empty, and both rendered below the email-footer menu an admin builds. {% block emailUnsubscribe %} is where a prospection email places its own way out (article L34-5 CPCE); a transactional one leaves it empty.

EmailLayoutProvider implements UiBundle's EmailLayoutProviderInterface, so any EmailTemplate sent anywhere in the ecosystem comes out in this bundle's branded layout — and in UiBundle's plain fallback on a site not installing it. wrap(string $bodyHtml, ?string $locale = null) receives the language the body was resolved in and hands it to the template, which passes it to each email_template_body() call: without it the four sentences followed whichever row the database returned first. Do not write an email layout in a satellite bundle; send with wrapLayout: true and this provider answers.

Footer components

<twig:c975LSite:General:HostedBy/>
<twig:c975LSite:General:MadeBy/>

Both read site-hosted-by-* / site-made-by-* from the config, and what they show comes from display-hosted-by / display-made-by (none, logo, name, logo-name) through ConfigBundle's credits_mode(). MadeBy words its label through made_by_label(), reading made-by-wording (made → "Réalisé par", powered → "Propulsé par" for a site only running the system). site-preconnect is a JSON array of external origins to preconnect to, read by core-bundle's layout, which adds the Matomo origin to it on its own.

Matomo, the cookie banner and the scroll buttons belong to UiBundle, and its layout renders them — <twig:c975LUi:Analytics:Matomo/>, <twig:c975LUi:Cookie:Consent/> and <twig:c975LUi:Scroll:Buttons/>. Rendered by the layout and not by the footer, so a site overriding footer keeps its tracking and its cookie banner. The first two carry their own enable guard; the buttons point at the id="top" set on <body> and at the <span id="bottom"> closing it, so a layout of your own keeps both anchors.

Do not

  • Do not ship a page layout from a satellite bundle, and do not {% extends '@c975LSite/…' %} with the @ prefix from one — extend layout.html.twig by name.
  • Do not replace the scaffolded layout without defining container and reading title/robots — the database pages render empty, silently.
  • Do not write a style="" attribute in a template. The nonce makes it inert.
  • Do not read app.flashes outside the layout's guard. The read opens a session for every visitor.
  • Do not import a stylesheet from assets/app.js. CSP blocks the whole entrypoint.
  • Do not declare a theme token on :root alone. Use :root, [data-theme].
  • Do not set colors or fonts in a theme file — they belong to the admin, in the config screen.
  • Do not paint text, an outline or a rule with var(--primary). Read var(--primary-ink), which is what dark mode lightens; --primary is the fill a surface needs.
  • Do not call another bundle's site-wide band function from the layout. Include its template with ignore_missing.
  • Do not write a <head> tag in this bundle's layout. Core-bundle's is the only one, and a copy here drifts from it without a word.
  • Do not override title to hide a page heading — it is the <title> tag. Override heading.
  • Do not hardcode the email copy. The four layout_* templates are the site's own, one row per language.
  • Do not make an email's paths absolute template by template. fullLayout already does it for all.
  • Do not read a color-mix() token in an email stylesheet. No mail client resolves it and the inliner drops the declaration — state the value, as sass/_email-footer.scss does for --label-color.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator