How to Customize GLPI's Visual Identity with the Branding Module

GLPI branding that survives updates: why editing the core wipes the brand on update, the real login screen DOM in GLPI 11 (span.glpi-logo, body.welcome-anonymous), the CSS the module injects through a hook, an audit SQL query, and the field pitfalls.

Swapping the GLPI logo looks like a five-minute tweak - until the next update wipes it out. Customizing the visual identity by editing core CSS or the login template works today, but it is a debt that comes due on its own: it disappears on update and, in the jump from GLPI 10 to 11, the login page DOM was renamed, so the old hack stops applying without throwing any error. This guide shows how to do branding that survives updates, where each piece lives in GLPI 11, and the pitfalls that only surface in a client environment.

Branding in GLPI is not just cosmetics

A GLPI that wears the organization's face reduces friction: the end user recognizes it as the company Service Desk, not a strange system asking for a login. But the hidden cost is not in picking the right color - it is in how the customization is applied. There are three paths, and two of them charge you dearly later.

  • Edit the core (CSS, images or templates inside glpi/css, glpi/pics or templates/): it works, but the next git pull or release extraction overwrites the files. The customization evaporates and nobody remembers why.
  • Theme/CSS injected from outside: it survives the file update, but it is tied to that version's selectors. When GLPI changes the HTML structure - as it did from 10 to 11 - the selector no longer exists and the CSS becomes dead letter, silently.
  • Plugin hook: GLPI exposes official injection points (display_login, add_javascript). The module applies the brand through those hooks, without touching a single core file. It is the only path that survives updates by design.

What you can customize - and where each piece lives

Every visual element of GLPI lives in a different place in the DOM, and swapping it without knowing the right target is what generates most of the "I changed it and nothing happened" tickets. The matrix below maps each piece to its config key and to the real mechanism in GLPI 11:

PieceConfig keyMechanism in GLPI 11Common pitfall
Faviconfavicon_picture<link rel="icon"> rewritten via JSAggressive browser cache holds the old icon
Tab titlepage_titledocument.titleScreens that set their own title revert it
Login logologin_picturespan.glpi-logo via content: url(...)Targeting .login-logo, which does not exist in 11
Login backgroundlogin_backgroundbody.welcome-anonymous / .page-anonymousTargeting body.login-page, which does not exist in 11
Header logointernal_pictureInternal header via add_javascriptConfusing it with the login logo (distinct targets)
Footerfooter_mode / footer_text.copyright (default theme in 11)Pasting unsanitized HTML into the custom text
Interface colorscolor_*Tabler CSS variables (--tblr-*)Passing a non-hex value and injecting broken CSS

The GLPI 11 pitfall: the login logo is not a background

While sustaining client environments, the classic ticket is: "we updated GLPI and the login screen reverted to default". The cause is almost always a customization made directly in the core or in CSS that targeted GLPI 10 selectors. In the jump to 11, the login screen template (page_card_notlogged.html.twig) was rewritten: the <body> became body.welcome-anonymous, the container became .page-anonymous, and the logo stopped being a background image - it is now a <span class="glpi-logo"> rendered by content: var(--logo). We found this out in practice on 2026-06-10, debugging why a client's login background simply would not apply after the upgrade: the old body.login-page selector no longer existed in the DOM. There was no error in the log - the CSS was correct for a version that no longer ran. That is why we now emit the right selectors through a hook, versioned alongside the module. This is the real CSS the module injects - and the dead selectors that explain the disappearance:

/* GLPI 11 - REAL login screen selectors (page_card_notlogged.html.twig) */
/* The logo is NOT a background: it is a <span class="glpi-logo"> with content: var(--logo) */
.page-anonymous .glpi-logo {
  content: url("/plugins/nextool/front/module_assets.php?module=branding&file=...") !important;
  width: 240px !important;
  height: 130px !important;
  object-fit: contain !important;   /* preserves the image aspect ratio */
}

/* The background goes on body.welcome-anonymous / .page-anonymous - NEVER on body.login-page */
body.welcome-anonymous,
.page-anonymous {
  background-image: url("...") !important;
  background-size: cover !important;
  background-position: center center !important;
}

/* GLPI 10 selectors that NO LONGER exist in 11 (why the old hack vanishes with no error):
   .login-logo      -> now span.glpi-logo
   body.login-page  -> now body.welcome-anonymous  */

How the module applies without touching the core

On the login screen there is no session, so the module hooks into display_login and injects an inline <style>/<script> block. The images (favicon, logo, background) are served by a stateless endpoint that requires no login, with three protections worth naming because they are exactly where careless plugins leak: the extension goes through a whitelist (png, jpg, jpeg, gif, ico, svg, webp), the path is resolved with realpath() and checked against the upload directory prefix (path traversal block), and the file is saved with a unique name via uniqid() so it neither collides nor overwrites. On internal, already-authenticated pages, the customization comes in through the add_javascript hook.

One performance detail that only shows up when you measure: the internal JS ships with Cache-Control: private, max-age=86400 and the images with public, max-age=604800 (one week), but with no ETag. The reason is concrete - with ETag and max-age together, Chrome revalidated (HTTP 304) on every page load, and each 304 still paid the full GLPI bootstrap on the server. A hash of the configuration goes into the URL's fv= parameter, so saving the config changes the URL and invalidates the cache instantly, with no revalidation needed.

Configuration in five steps

  1. Enable the Branding module in the NexTool module catalog.
  2. In the Settings tab, upload the favicon, the login logo and the header logo; set the width and height of each logo (accepts px, em, rem, %, vw, vh).
  3. Upload the login screen background and adjust the browser tab title.
  4. Choose the footer behavior: keep the original, hide it, or replace it with text (HTML limited to safe tags).
  5. Optional: set the interface colors via CSS variables (the module validates each value as hex before emitting it, preventing CSS injection). Reload GLPI to see the result.

Auditing: who changed what

Every action - enable, disable, save config, upload or delete an image - is recorded in a module log table. In an environment with more than one administrator, this trail answers "who changed the logo on Friday night" without relying on memory. The diagnostic query:

-- Branding module audit trail: who changed the visual identity and when
SELECT l.date_creation  AS when_at,
       u.name           AS user_name,
       l.action         AS action,   -- save_config, upload_picture, delete_picture, toggle_enable/disable
       l.detail         AS detail
FROM   glpi_plugin_nextool_branding_log l
LEFT   JOIN glpi_users u ON u.id = l.users_id
ORDER  BY l.date_creation DESC
LIMIT  20;

Field pitfalls

  • Favicon that will not change - the browser caches the favicon aggressively, per tab and per session. After uploading the new icon, a hard refresh (Ctrl+Shift+R) or closing and reopening the tab fixes it; it is not a module bug.
  • SVG from a trusted source - SVG is great for a logo (scales without losing sharpness), but it can carry embedded script. Use SVG only from art you generated yourself; the endpoint serves the file with the correct Content-Type, but the hygiene starts at upload time.
  • Footer with pasted HTML - the custom footer text goes through sanitization that allows only a handful of tags (a, b, strong, i, em, span, br). Pasting a whole block of marketing HTML simply will not render what you expect.
  • Logo aspect ratio - setting fixed width and height without respecting the original ratio distorts the image. The module applies object-fit: contain precisely to avoid stretching, but check it on screen.
  • Upgrade 10 to 11 - if the client already had manual branding on GLPI 10, it will disappear in 11 because the selectors changed. Treat branding as part of the upgrade plan, not as a post-migration surprise.

Want to standardize GLPI's visual identity across every environment you manage, without reopening the subject on each update? The Branding module is free and applies the brand through native hooks. And if you would rather outsource GLPI deployment and sustaining, NexTool maintains client environments with versioned, update-proof branding.


Este conteúdo foi produzido com auxílio de inteligência artificial e revisado pela equipe Nextool Solutions.

Frequently Asked Questions

Because the customization was made directly in the core (CSS files, images or templates), and the GLPI update overwrites those files. The way to survive updates is to apply the brand through plugin hooks (display_login, add_javascript), without touching the core - which is exactly what the Branding module is for.

In GLPI 11 the login screen template was rewritten. The logo is no longer a background image; it became a <span class="glpi-logo"> with content: var(--logo); the body became body.welcome-anonymous and the container .page-anonymous. If the CSS targets GLPI 10 selectors (.login-logo, body.login-page), they no longer exist in the DOM and the rule does not apply, with no error thrown.

No. The Branding module applies favicon, logos, login background, footer and colors through the interface, via native hooks. The changes do not live in core files, so they survive GLPI updates.

In the module's Settings tab, upload the favicon image and type the desired title. The module rewrites the <link rel="icon"> and document.title via JavaScript. If the new icon does not appear, it is browser cache: a hard refresh (Ctrl+Shift+R) or reopening the tab fixes it.

Yes, SVG is accepted and scales without losing sharpness. The upload goes through an extension whitelist and the service guards against path traversal. Since SVG can contain embedded script, use only art from a trusted source (ideally generated by you), not files of unknown origin.

Yes. Branding is free, part of NexTool and compatible with GLPI 10 and 11. Watch out on upgrade: if there was manual branding on 10, it does not migrate to 11 on its own (the login screen selectors changed); reapply it through the module after migration.

Need help?