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
- Duplication: The mots-clés bulk logic in
contenus.php(~130 lines) is a near-identical copy of the tags bulk logic intags.php(~76 lines) and the langues bulk logic in the samecontenus.php(~130 lines). Three copies of the same pattern. - PHP-in-JS coupling:
acces.phpinjects 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">. - No build step: There is no bundler, no minification, no tree-shaking. The
biome.jsonconfig only handles formatting/linting of JS (not CSS). - Dev-only code in production:
footer.phphasconsole.logcalls in HTMX event handlers, andhead.phphas the live-reload poller (gated, but still present in templates served in production). Thefooter.phpconsole.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
@importchains block rendering:style.cssuses 17@importstatements. Browsers downloadstyle.css, discover the imports, then fetch each imported file sequentially. This is the worst way to load CSS for performance — it creates a waterfall.@importis essentially deprecated for production use.- No minification: Custom CSS files are served uncompressed. The
@import-based structure makes them hard to bundle or minify automatically. - 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$extraCsspattern).
3.3 Recommendation: Bundle + Minify
Create a build step that:
- Concatenates all CSS files into a single bundle (one for public, one for admin, one for forms).
- Minifies the result (CSSNano, LightningCSS, or even a simple PHP script).
- Replaces all
@importwith actual concatenation. - The
@importapproach 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
- No minification on custom JS: All 9 app JS files are served uncompressed. Combined they're ~1,763 lines (~45 KB unminified).
- 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.
- 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.