Skip to content

Commit 88697bd

Browse files
committed
Stop the panel's own scripts being cached for a year
The document root caches every .js and .css for a year as immutable. That is right for the site's build output, where a changed file is a changed filename because the name carries a content hash. It is wrong for the panel: admin.js, admin.css, api.js, car-data.js and booking-data.js have fixed names, so a browser that loaded the panel once would not ask for them again until 2027 -- and every change to the panel would reach nobody who had ever used it, with no error and nothing to notice. The vehicle photograph upload merged minutes ago would have been the first casualty. Two parts, because one alone does not finish the job. admin/.htaccess overrides the header for this folder, which fixes it for browsers that ask -- and a browser already holding the file will not ask. So the tags now carry the file's modification time: a changed file is a different URL, and the cached copy cannot match it. asset() lives in its own src/assets.php. It went into http.php first, which was wrong in a way worth recording: http.php is the JSON endpoints' plumbing, and no page that renders HTML loads it. dashboard.php, index.php and content.php all require csrf.php and nothing else, so asset() was defined precisely where it was never called from and every panel page would have died on an undefined function. Caught by loading the pages rather than by reading them -- php -l passes on a file that calls a function that does not exist. Verified: asset() returns admin.css?v=<mtime> and falls back to ?v=0 for a file that is not there; the require precedes the first call in all three pages; index.php reaches its database call, which is as far as anything gets here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
1 parent bb81a02 commit 88697bd

5 files changed

Lines changed: 62 additions & 7 deletions

File tree

‎admin/.htaccess‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@
88
# its contents, which is how the layout of a panel gets mapped.
99
Options -Indexes
1010

11+
# Undo the document root's caching rule for this folder.
12+
#
13+
# The site's build assets carry a content hash in the filename, so a changed
14+
# file is a changed URL and the root .htaccess caches every .js and .css for a
15+
# year as immutable. Correct for those. Fatal for these: admin.js, admin.css,
16+
# api.js and car-data.js have fixed names, so a browser that has them once
17+
# would never ask again -- and every change to the panel would reach nobody who
18+
# had ever used it, with no error and nothing to notice.
19+
#
20+
# Revalidated on each load instead. It costs one conditional request per file
21+
# and it is the difference between shipping a change and appearing to.
22+
<IfModule mod_expires.c>
23+
<FilesMatch "\.(js|css)$">
24+
ExpiresActive On
25+
ExpiresDefault "access plus 0 seconds"
26+
</FilesMatch>
27+
</IfModule>
28+
<IfModule mod_headers.c>
29+
<FilesMatch "\.(js|css)$">
30+
Header set Cache-Control "no-cache, must-revalidate"
31+
</FilesMatch>
32+
</IfModule>
33+
1134
<IfModule mod_authz_core.c>
1235
# config.php holds the database password. PHP executes it rather than
1336
# printing it, so this changes nothing while PHP is healthy -- it matters

‎admin/content.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
declare(strict_types=1);
33

44
require_once __DIR__ . '/src/csrf.php';
5+
require_once __DIR__ . '/src/assets.php';
56
require_once __DIR__ . '/src/content.php';
67
require_once __DIR__ . '/src/migrate.php';
78
require_once __DIR__ . '/src/audit.php';
@@ -105,7 +106,7 @@ function field_input(string $name, array $spec, mixed $value): void
105106
<title>Website content — NiteSha Cars</title>
106107
<meta name="robots" content="noindex, nofollow" />
107108
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔐</text></svg>" />
108-
<link rel="stylesheet" href="admin.css" />
109+
<link rel="stylesheet" href="<?= asset('admin.css') ?>" />
109110
<style>
110111
.c-wrap { max-width: 56rem; margin: 0 auto; padding: 1.5rem 1rem 4rem; }
111112
.c-top { display: flex; flex-wrap: wrap; gap: 1rem; align-items: baseline; justify-content: space-between; margin-bottom: 1.25rem; }

‎admin/dashboard.php‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
declare(strict_types=1);
33

44
require_once __DIR__ . '/src/csrf.php';
5+
require_once __DIR__ . '/src/assets.php';
56

67
// Anyone reaching this page must already be signed in; require_login sends
78
// them to the sign-in form otherwise.
@@ -22,7 +23,7 @@
2223
<link rel="preconnect" href="https://fonts.googleapis.com">
2324
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap" rel="stylesheet">
2425
<meta name="csrf-token" content="<?= e(csrf_token()) ?>">
25-
<link rel="stylesheet" href="admin.css" />
26+
<link rel="stylesheet" href="<?= asset('admin.css') ?>" />
2627
</head>
2728
<body>
2829

@@ -751,9 +752,9 @@
751752
</div>
752753
</div>
753754

754-
<script src="api.js"></script>
755-
<script src="car-data.js"></script>
756-
<script src="booking-data.js"></script>
757-
<script src="admin.js"></script>
755+
<script src="<?= asset('api.js') ?>"></script>
756+
<script src="<?= asset('car-data.js') ?>"></script>
757+
<script src="<?= asset('booking-data.js') ?>"></script>
758+
<script src="<?= asset('admin.js') ?>"></script>
758759
</body>
759760
</html>

‎admin/index.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
declare(strict_types=1);
33

44
require_once __DIR__ . '/src/csrf.php';
5+
require_once __DIR__ . '/src/assets.php';
56

67
// Already signed in — go straight through.
78
if (current_user() !== null) {
@@ -44,7 +45,7 @@
4445
<meta name="robots" content="noindex, nofollow">
4546
<link rel="preconnect" href="https://fonts.googleapis.com">
4647
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap" rel="stylesheet">
47-
<link rel="stylesheet" href="admin.css">
48+
<link rel="stylesheet" href="<?= asset('admin.css') ?>">
4849
</head>
4950
<body>
5051

‎admin/src/assets.php‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* URLs for the panel's own stylesheet and scripts.
6+
*
7+
* admin.js and admin.css have fixed names, unlike the site's build output
8+
* which carries a content hash in the filename. The document root caches every
9+
* .js and .css for a year as immutable -- correct for hashed files, fatal for
10+
* these: a browser that loaded the panel once would never fetch them again, so
11+
* a change reached nobody who had ever used it, with no error and nothing to
12+
* notice.
13+
*
14+
* admin/.htaccess now overrides that header, but only for browsers that ask,
15+
* and one already holding the file will not ask until next year. The
16+
* modification time in the query string is what actually breaks that: a
17+
* changed file is a different URL, so the cached copy cannot match.
18+
*
19+
* Its own file rather than http.php, which is the JSON endpoints' plumbing and
20+
* is not loaded by any page that renders HTML. Putting it there defined it
21+
* exactly where it was never called from.
22+
*/
23+
24+
function asset(string $file): string
25+
{
26+
$path = __DIR__ . '/../' . $file;
27+
$stamp = is_file($path) ? (string) filemtime($path) : '0';
28+
return $file . '?v=' . $stamp;
29+
}

0 commit comments

Comments
 (0)