Files
xamxam/docs/ANALYSIS_INLINE_JS_CSS_MINIFY.md

14 KiB

Analysis: Inline JS/CSS, Minification & Compression

Date: 2026-06-24 Scope: Entire project (excluding /vendor, /.jj, /.git, /coverage, /storage)


1. Inline JavaScript

1.1 Summary

~797 lines of inline JavaScript spread across 17 PHP template files. Every admin page loads multiple inline blocks. The public-facing pages also contain inline JS.

1.2 Detailed Inventory

File Lines Can Be Externalized? Notes
app/templates/admin/contenus.php 232 Yes Bulk lang/tag operations: delete, merge, rename, inline rename via HTMX, dialog handlers. Two large independent blocks (langues + mots-clés) that mirror each other almost identically.
app/templates/public/repertoire.php 101 Yes Student popover: positioning logic, HTML fetch/prefill. Self-contained component.
app/templates/admin/tags.php 76 Yes Bulk merge, inline rename, delete confirm. Mirrors the mots-clés block in contenus.php — same logic duplicated.
app/templates/admin/apropos-groups-form.php 68 Yes Dynamic form fields: add/remove contact groups and entries, reindex. Reads template from <template> element. Could be a reusable module.
app/templates/admin/acces.php 59 ⚠️ Mostly Some JS is straightforward (dialogs, clipboard). But 4 lines pass PHP vars into JS globals (_newLinkPassword, _newLinkSlug) → needs a pattern like <meta> tags or data-* attributes or a tiny inline bootstrap.
app/templates/partials/form/jury-fieldset.php 42 Yes Dynamic jury member rows: add/remove, autocomplete init, load state restore.
app/templates/partials/form/form.php 41 Yes Duration unit toggle (pages/mo/durée → time fields), flash warning scroll.
app/templates/admin/contenus-edit.php 36 Yes Sidebar link add/remove rows, reindex. Simple list manipulation.
app/templates/admin/partials/admin-toc.php 35 Yes IntersectionObserver for sticky TOC highlighting. Pure JS, no PHP dependency.
app/templates/admin/acces-etudiante.php 29 Yes Dialog openers, clipboard copy, password dialog. Standard UI helpers.
app/templates/admin/footer.php 27 ⚠️ Mixed HTMX global event listeners (sendError, beforeSend, afterSettle) + MD cheatsheet dialog handling. The HTMX debug logging is dev-only and should probably be conditional or removed in production.
app/templates/partials/form/language-search.php 17 Yes Language pill input: search, select, remove. Self-contained interactive widget.
app/templates/admin/index.php 10 Yes Bulk selection toggle, updateBulk, confirmBulk. Already inlined but tiny.
app/templates/admin/parametres.php 10 Yes SMTP error field focus on load + sys-status collapse toggle.
app/templates/admin/file-access.php 8 Yes Dialog openers for approve/reject. Trivial.
app/templates/admin/index-table.php 1 Yes One-liner re-attaching change listeners after HTMX swap.
app/templates/head.php 5 Must stay inline Live-reload poller (dev only). Already gated behind php_sapi_name() === 'cli-server'.

1.3 Key Observations

  1. Duplication: The mots-clés bulk logic in contenus.php (~130 lines) is a near-identical copy of the tags bulk logic in tags.php (~76 lines) and the langues bulk logic in the same contenus.php (~130 lines). Three copies of the same pattern.
  2. PHP-in-JS coupling: acces.php injects PHP values ($baseUrl, $newLinkPassword, $newLinkSlug) directly into JS globals. This is fragile. Alternatives: <meta> tags, data-* attributes, or a JSON blob in a <script type="application/json">.
  3. No build step: There is no bundler, no minification, no tree-shaking. The biome.json config only handles formatting/linting of JS (not CSS).
  4. Dev-only code in production: footer.php has console.log calls in HTMX event handlers, and head.php has the live-reload poller (gated, but still present in templates served in production). The footer.php console.log calls are unconditional.

2. Inline CSS

2.1 Summary

4 locations, all in standalone error/maintenance pages that are served without the main CSS pipeline:

File Line count Purpose
app/public/maintenance.php ~20 lines (inline in <style>) 503 Maintenance page — dark minimal style
app/public/validate-access.php ~16 lines (2 blocks) Access token validation page + error page
app/src/Controllers/SearchController.php ~15 lines (in PHP heredoc) Rate-limit error page (429)

2.2 Assessment

These are acceptable as inline styles:

  • Each page is a standalone error/status page that must render correctly without the main CSS pipeline (no style.css, no external deps).
  • Each is ~15-20 lines, fully self-contained, and has zero overlap with the main design system.
  • Moving them to external files would add an extra HTTP request for pages almost nobody sees, without benefit.

Recommendation: Keep as-is. These are the correct use case for inline CSS.

2.3 Edge Case: SearchController.php

The rate-limit 429 page is rendered as a PHP heredoc inside a controller method. This could be extracted to a template file (app/templates/error/rate-limit.php) for consistency, but functionally it's fine.


3. CSS Architecture & Minification

3.1 Current Setup

style.css  (27 lines, @import-only)
  ├── reset.css, colors.css, typography.css, base.css
  ├── components/{links,focus,forms,tables,dialog,details,media,buttons,badges,toast,pagination,header,search}.css
  └── utilities.css

+ admin.css       (loaded in admin via $extraCss)
+ form-base.css   (loaded on form pages)
+ form-admin.css
+ public.css, repertoire.css, tfe.css, content-page.css, system.css, file-access.css
+ filepond.min.css + plugin (vendor, already minified)
+ modern-normalize.min.css (vendor, already minified)

Total CSS (excluding vendor minified): ~6,200 lines across 18 files, served as 2-4 requests per page (style.css via @import + page-specific files via <link>).

3.2 Problems

  1. @import chains block rendering: style.css uses 17 @import statements. Browsers download style.css, discover the imports, then fetch each imported file sequentially. This is the worst way to load CSS for performance — it creates a waterfall. @import is essentially deprecated for production use.
  2. No minification: Custom CSS files are served uncompressed. The @import-based structure makes them hard to bundle or minify automatically.
  3. No cache-busting on CSS: The App::assetV() helper adds version query strings for JS files, but the same mechanism handles CSS. Need to verify it's consistently applied (appears to be, via $extraCss pattern).

3.3 Recommendation: Bundle + Minify

Create a build step that:

  1. Concatenates all CSS files into a single bundle (one for public, one for admin, one for forms).
  2. Minifies the result (CSSNano, LightningCSS, or even a simple PHP script).
  3. Replaces all @import with actual concatenation.
  4. The @import approach was a good dev ergonomics choice but should be resolved at build time, not at request time.

4. JavaScript Architecture & Minification

4.1 Current Setup

Custom JS: ~1,763 lines across 9 files (all in app/public/assets/js/app/):

File Lines Purpose
file-upload-filepond.js 1,057 FilePond init/config for admin + partage
pill-search.js 197 Language/tag pill input widget
jury-autocomplete.js 152 Jury member autocomplete
access-request.js 101 File access request flow
autosave-handler.js 79 OverType autosave
admin-logs.js 70 Admin log viewer
acces-password.js 36 Share link password prompt
beforeunload-guard.js 32 Unsaved changes warning
clipboard.js 39 Copy-to-clipboard utility

Vendor JS (already minified): htmx, FilePond + 4 plugins, OverType.

4.2 Problems

  1. No minification on custom JS: All 9 app JS files are served uncompressed. Combined they're ~1,763 lines (~45 KB unminified).
  2. No bundling: 9 separate HTTP requests for app JS on form pages (plus 6 vendor scripts = 15 total on the partage form page). Each is a round-trip.
  3. Vendor scripts already minified: Good. No action needed there.

4.3 Recommendation: Minify + Optionally Bundle

At minimum: minify each app JS file individually. This is the lowest-risk change and yields most of the benefit (~40-60% size reduction on custom JS).

Optionally: bundle all app JS into one file. But this is lower priority — the 9 individual files are small and HTTP/2 multiplexing handles them fine. The bigger win is just minification.


5. Gzip / Compression

5.1 Current State: NOT ENABLED

The nginx configuration (nginx/xamxam.conf) has no gzip directives whatsoever. Neither does the reference config. There is no gzip on;, no gzip_types, nothing.

5.2 Impact

This means every CSS file (~6,200 lines uncompressed), every JS file (~1,763 lines uncompressed), every HTML page, and every API response is served without compression. For a text-heavy PHP application, this is the single biggest performance miss.

Typical compression ratios for text assets:

  • HTML: 70-80% reduction
  • CSS: 75-85% reduction
  • JS: 70-80% reduction
  • JSON/XML: 80-90% reduction

5.3 Recommendation

Add gzip to the nginx config. Brotli would be even better but requires ngx_brotli module (not always available). Gzip is universally supported and the default nginx module is always available.

Recommended nginx gzip config:

# Compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
    text/plain
    text/css
    text/javascript
    application/javascript
    application/json
    application/xml
    text/xml
    image/svg+xml
    application/x-font-ttf
    font/opentype;

This should be added in the server block (or the http block of the main nginx.conf).


6. Priority Summary

Priority Item Effort Impact
🔴 P0 Add gzip to nginx config 5 min (3 lines of config) High — 70-80% bandwidth reduction on all text assets
🟠 P1 Minify custom JS files 1-2 hours (add a build step) Medium — ~40-60% JS size reduction
🟠 P1 Bundle CSS (eliminate @import) 2-3 hours Medium — eliminates render-blocking waterfall
🟡 P2 Extract inline JS to external files 4-6 hours Low-Medium — enables caching, CSP tightening
🟡 P2 Remove dev-only console.log from footer.php 5 min Low — code quality
🟢 P3 Bundle JS files (single file) 2-3 hours Low — HTTP/2 handles multiple small files fine
🟢 P3 Deduplicate mots-clés/langues/tags JS 2-3 hours Low — maintenance benefit

7. Detailed Extraction Plan (for P2)

If inline JS externalization is pursued, here's the recommended mapping:

New files to create:

New File Content From
assets/js/app/admin-bulk-actions.js index.php bulk selection + index-table.php reattach
assets/js/app/admin-contenus-langues.js contenus.php langues block (delete, rename, merge, inline rename)
assets/js/app/admin-contenus-motscles.js contenus.php mots-clés block (mirrors above)
assets/js/app/admin-tags.js tags.php tags bulk + inline rename
assets/js/app/admin-contacts-form.js apropos-groups-form.php dynamic group/entry management
assets/js/app/admin-acces-sharelink.js acces.php + acces-etudiante.php (clipboard, dialogs, edit, archive, password)
assets/js/app/admin-toc.js admin-toc.php IntersectionObserver
assets/js/app/admin-file-access.js file-access.php approve/reject dialogs
assets/js/app/repertoire-popover.js repertoire.php student popover
assets/js/app/form-duration-toggle.js form.php duration unit toggle
assets/js/app/form-jury-fields.js jury-fieldset.php dynamic jury rows
assets/js/app/form-language-search.js language-search.php language pill widget
assets/js/app/sidebar-links-editor.js contenus-edit.php sidebar links add/remove
assets/js/app/htmx-global-setup.js footer.php HTMX event listeners
assets/js/app/smtp-error-focus.js parametres.php SMTP field focus
assets/js/app/sys-status-toggle.js parametres.php collapsible toggle

PHP-to-JS data passing

For acces.php which injects _newLinkPassword and _newLinkSlug into JS globals, replace with:

<meta name="new-link-password" content="<?= htmlspecialchars($newLinkPassword ?? '') ?>">
<meta name="new-link-slug" content="<?= htmlspecialchars($newLinkSlug ?? '') ?>">

Then read from document.querySelector('meta[name="new-link-slug"]').content in the external JS.


8. Build Step Proposal

A minimal build step using existing infrastructure (no npm/node required):

Option A: PHP build script (zero new dependencies)

// scripts/build-assets.php
// 1. Minify JS files using a simple regex-based minifier (strip comments, whitespace)
// 2. Concatenate CSS files, replacing @import
// 3. Write to app/public/assets/dist/

Option B: Justfile commands using CLI tools

# Requires: uglifyjs or terser (npm), lightningcss (npm/cargo)
build-js:
    uglifyjs app/public/assets/js/app/*.js -o app/public/assets/dist/app.min.js -c -m

build-css:
    lightningcss --minify --bundle app/public/assets/css/style.css -o app/public/assets/dist/style.min.css

Option C: justfile + Python (available on any server)

Python's html.parser and re can handle CSS concatenation + JS minification without any additional packages.


9. Security Note: CSP Implications

The current CSP allows 'unsafe-inline' for scripts and styles:

script-src 'self' 'unsafe-inline' 'unsafe-eval'
style-src 'self' 'unsafe-inline'

This is required as long as inline scripts and styles exist. Externalizing inline JS/CSS would allow tightening the CSP to remove 'unsafe-inline' (using nonces or hashes), which is a meaningful security improvement against XSS. However, HTMX's hx-on:* attributes also require 'unsafe-inline' or a nonce-based approach, so full removal isn't trivial.