mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
277 lines
14 KiB
Markdown
277 lines
14 KiB
Markdown
# 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:**
|
|
|
|
```nginx
|
|
# 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:
|
|
|
|
```html
|
|
<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)
|
|
```php
|
|
// 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
|
|
```makefile
|
|
# 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.
|