Merge pull request #834 from BKPepe/fix/modern-frontend-asset-paths

fix: preserve the modern frontend layout in deployments (#834)

Serve the modern UI assets from frontend/ in both repository and Docker
deployments, matching the paths used by index-modern.html.

Move settings.json and server-list.json to the web root, where the modern
page resolves its configuration, and keep Docker, installation guidance, and
design-switch documentation consistent with that layout.

Add E2E coverage for:
- serving the repository unchanged with Python's static HTTP server;
- required modern UI assets and a real 404 for missing paths;
- Alpine standalone settings and modern frontend rendering.

Fixes #804
This commit is contained in:
sstidl 2026-08-11 08:53:10 +02:00 • committed by GitHub
commit f3df5db6ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 106 additions and 43 deletions

View file

@ -15,23 +15,19 @@ By default, LibreSpeed uses the **classic design** (located in `index-classic.ht
- **`frontend/`** - Directory containing modern design assets (CSS, JS, images, fonts) - kept for non-Docker deployments
### File Structure (Docker)
In Docker deployments, the frontend assets are flattened to root-level subdirectories:
Docker deployments preserve the same layout as non-Docker deployments:
- **`index.html`** - Entry point (lightweight switcher)
- **`index-classic.html`** - Classic design
- **`index-modern.html`** - Modern design
- **`styling/`** - CSS files for modern design
- **`javascript/`** - JS files for modern design
- **`images/`** - Images for modern design
- **`fonts/`** - Fonts for modern design
- **No `frontend/` directory** - Assets are copied directly to root subdirectories
- **`index-classic.html`** - Classic design at root
- **`index-modern.html`** - Modern design at root
- **`frontend/`** - Modern design assets, copied into the web root unchanged
- **`settings.json` and `server-list.json`** - Configuration files at root, next to `index-modern.html`
### Benefits of Root-Level Design Files
✅ Both designs at same level - no path confusion
✅ `results/` accessible from both designs with same relative path
✅ `backend/` accessible from both designs with same relative path
✅ No subdirectory nesting issues
✅ Clean separation of concerns
✅ Docker containers have no `frontend/` parent directory
### Benefits of the Shared Layout
✅ Docker and non-Docker deployments use the same paths
✅ Both designs are at the same level
✅ `results/` and `backend/` use the same relative paths from both designs
✅ The modern design loads assets consistently from `frontend/`
✅ Configuration files stay at the web root, where the modern page expects them
## Browser Compatibility
@ -73,9 +69,9 @@ URL parameters take precedence over the configuration file, making them useful f
### Docker Deployments
- **Entry Point**: Root `index.html` file (lightweight redirect page)
- **Old Design**: `index-classic.html` at root
- **New Design**: `index-modern.html` at root (references assets in root subdirectories)
- **Assets**: Frontend assets copied directly to root subdirectories (`styling/`, `javascript/`, `images/`, `fonts/`)
- **No `frontend/` directory** - Assets are flattened to root level
- **New Design**: `index-modern.html` at root (references assets in `frontend/` subdirectory)
- **Assets**: Frontend assets in `frontend/`, copied into the web root unchanged
- Same layout as a non-Docker deployment, so the two cannot drift apart
Both designs are at the same directory level, ensuring that relative paths to shared resources like `backend/` and `results/` work correctly for both.
@ -93,4 +89,4 @@ Both design HTML files are at the root level, eliminating path issues.
The modern design references assets from the `frontend/` subdirectory (e.g., `frontend/styling/index.css`), while both designs can access shared resources like `backend/` and `results/` using the same relative paths.
### Docker
In Docker deployments, the `frontend/` directory is flattened during container startup. Assets are copied directly to root-level subdirectories (`styling/`, `javascript/`, `images/`, `fonts/`), and `index-modern.html` references these root-level paths. This eliminates the `frontend/` parent directory in the container.
In Docker deployments, `frontend/` is copied into the web root as it stands during container startup, so the container serves the same layout the repository has and the same paths `index-modern.html` asks for.

View file

@ -27,6 +27,8 @@ COPY index.html /speedtest/
COPY index-classic.html /speedtest/
COPY index-modern.html /speedtest/
COPY config.json /speedtest/
COPY settings.json /speedtest/
COPY server-list.json /speedtest/
COPY stability.html /speedtest/
COPY favicon.ico /speedtest/
COPY manifest.webmanifest /speedtest/

View file

@ -46,6 +46,8 @@ COPY index.html /speedtest/
COPY index-classic.html /speedtest/
COPY index-modern.html /speedtest/
COPY config.json /speedtest/
COPY settings.json /speedtest/
COPY server-list.json /speedtest/
COPY stability.html /speedtest/
COPY favicon.ico /speedtest/
COPY manifest.webmanifest /speedtest/

View file

@ -41,8 +41,7 @@ Works with mobile versions too.
Assuming you have PHP and a web server installed, the installation steps are quite simple.
1. Download the source code and extract it
1. Copy the project files to your web server's shared folder (ie. `/var/www/html/speedtest` for Apache). For the current layout, the web root should contain `index.html`, `index-classic.html`, `index-modern.html`, `stability.html`, `design-switch.js`, `config.json`, `speedtest.js`, `speedtest_worker.js`, `stability_worker.js`, `favicon.ico`, and the `backend` folder.
1. Also copy the contents of `frontend/` into the same web root so the modern UI assets end up in `styling/`, `javascript/`, `images/`, and `fonts/` next to the HTML files.
1. Copy the project files to your web server's shared folder (ie. `/var/www/html/speedtest` for Apache), keeping the layout as it is in the repository. The modern UI loads its assets from `frontend/`, so that directory is copied as a whole rather than unpacked.
1. Optionally, copy the results folder too, and set up the database using the config file in it.
1. Be sure your permissions allow read and execute access where needed.
1. Visit YOURSITE/speedtest/index.html and voila!

6
doc.md
View file

@ -197,18 +197,20 @@ Requirements:
* FreeType 2 and its PHP module
* The PHP gd library
To install the speed test frontend, copy the project files to your web server and keep the modern UI assets next to the HTML files:
To install the speed test frontend, copy the project files to your web server, keeping the layout they have in the repository:
* `index.html`
* `index-classic.html`
* `index-modern.html`
* `design-switch.js`
* `config.json`
* `settings.json`
* `server-list.json`
* `speedtest.js`
* `speedtest_worker.js`
* `favicon.ico`
* the `backend` folder
* the contents of `frontend/`, copied so `styling/`, `javascript/`, `images/`, and `fonts/` sit next to the HTML files
* the `frontend` folder, copied as a whole: the modern UI loads its assets from `frontend/`
* Optionally, the `results` folder
__Important:__ The speed test needs read and execute permissions in the installation folder where applicable!

View file

@ -82,15 +82,12 @@ if [[ "$MODE" == "frontend" || "$MODE" == "dual" || "$MODE" == "standalone" ]];
cp /speedtest/index-classic.html /var/www/html/
cp /speedtest/index-modern.html /var/www/html/
cp /speedtest/stability.html /var/www/html/
# Copy frontend assets directly to root-level subdirectories (no frontend/ parent dir)
mkdir -p /var/www/html/styling /var/www/html/javascript /var/www/html/images /var/www/html/fonts
cp -a /speedtest/frontend/styling/* /var/www/html/styling/
cp -a /speedtest/frontend/javascript/* /var/www/html/javascript/
cp -a /speedtest/frontend/images/* /var/www/html/images/
cp -a /speedtest/frontend/fonts/* /var/www/html/fonts/ 2>/dev/null || true
# Keep the frontend assets under frontend/, which is where the HTML looks for
# them and where they sit in the repository
cp -a /speedtest/frontend /var/www/html/
# Copy frontend config files
cp /speedtest/frontend/settings.json /var/www/html/settings.json 2>/dev/null || true
cp /speedtest/settings.json /var/www/html/settings.json 2>/dev/null || true
if [ -f /servers.json ]; then
echo "using mounted /servers.json for server-list.json"
cp /servers.json /var/www/html/server-list.json

View file

@ -5,15 +5,18 @@ This directory contains the modern LibreSpeed UI assets.
## Deployment
For installation and deployment, follow the top-level [README.md](../README.md)
and [DESIGN_SWITCH.md](../DESIGN_SWITCH.md). In non-Docker deployments, the
contents of this directory must be copied so `styling/`, `javascript/`,
`images/`, and `fonts/` sit next to the root HTML files.
and [DESIGN_SWITCH.md](../DESIGN_SWITCH.md). This directory is copied as a
whole: `index-modern.html` loads its assets from `frontend/`, so the layout
here is the layout that gets served.
## Configuration
- `server-list.json` contains the default server list used by the modern UI.
- `settings.json` overrides selected `speedtest_worker.js` settings.
- `index.html` and `index-modern.html` show how the frontend is wired up.
The two configuration files live at the top level rather than here, because the
page fetches them relative to itself:
- `../server-list.json` contains the default server list used by the modern UI.
- `../settings.json` overrides selected `speedtest_worker.js` settings.
- `index.html` and `../index-modern.html` show how the frontend is wired up.
## Notes

View file

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description"
content="Free and Open Source Speedtest. Run it right now in your browser, or self-host on a PHP, Golang, Rust or Node server. License: LGPL." />
<link rel="shortcut icon" href="images/favicon.svg" />
<link rel="shortcut icon" href="frontend/images/favicon.svg" />
<link rel="manifest" href="manifest.webmanifest" />
<link rel="apple-touch-icon" href="images/icon-192.png" />
<meta name="theme-color" content="#000000" />
@ -15,14 +15,14 @@
// Set this to a different URL to load the server list from another location.
var SPEEDTEST_SERVERS = "server-list.json";
</script>
<script type="text/javascript" src="javascript/index.js"></script>
<link rel="stylesheet" type="text/css" href="styling/index.css" />
<script type="text/javascript" src="frontend/javascript/index.js"></script>
<link rel="stylesheet" type="text/css" href="frontend/styling/index.css" />
<title>LibreSpeed - Free and Open Source Speedtest</title>
</head>
<body>
<header>
<img src="images/logo.svg" alt="LibreSpeed" />
<img src="frontend/images/logo.svg" alt="LibreSpeed" />
</header>
<main>
<h1>Free and Open Source Speedtest.</h1>
@ -31,7 +31,7 @@
<div class="server-selector">
<div class="chosen">
<div class="chevron">
<img src="images/chevron.svg" alt="select..." />
<img src="frontend/images/chevron.svg" alt="select..." />
</div>
<p>current server</p>
<h2 id="selected-server">searching nearest server...</h2>
@ -84,7 +84,7 @@
<dialog id="share">
<div class="close-dialog">
<img src="images/close-button.svg" alt="Close" />
<img src="frontend/images/close-button.svg" alt="Close" />
</div>
<img id="results" src="" alt="Test results in graphical form" />
<button id="copy-link">Copy link</button>
@ -92,7 +92,7 @@
<dialog id="privacy">
<div class="close-dialog">
<img src="images/close-button.svg" alt="Close" />
<img src="frontend/images/close-button.svg" alt="Close" />
</div>
<section>
<h1>Privacy Policy</h1>

View file

@ -18,6 +18,11 @@ module.exports = defineConfig({
},
globalSetup: require.resolve('./tests/e2e/global-setup.js'),
globalTeardown: require.resolve('./tests/e2e/global-teardown.js'),
webServer: {
command: 'python3 -m http.server 18184 --bind 127.0.0.1 --directory .',
url: 'http://127.0.0.1:18184',
reuseExistingServer: !process.env.CI,
},
projects: [
{
name: 'chromium',

View file

@ -31,6 +31,17 @@ services:
ports:
- "18185:8080"
standalone-alpine:
build:
context: ..
dockerfile: Dockerfile.alpine
environment:
- MODE=standalone
- WEBPORT=8080
- USE_NEW_DESIGN=true
ports:
- "18187:8080"
standalone-apostrophe:
build:
context: ..

View file

@ -68,6 +68,7 @@ module.exports = async () => {
const timeoutMs = 180_000;
await waitForReady('standalone', 'http://127.0.0.1:18180/index.html', timeoutMs);
await waitForReady('standalone-new', 'http://127.0.0.1:18185/index.html', timeoutMs);
await waitForReady('standalone-alpine', 'http://127.0.0.1:18187/index.html', timeoutMs);
await waitForReady('backend', 'http://127.0.0.1:18181/empty.php', timeoutMs);
await waitForReady('frontend', 'http://127.0.0.1:18182/index-modern.html', timeoutMs);
await waitForReady('dual', 'http://127.0.0.1:18183/index-modern.html', timeoutMs);

View file

@ -5,6 +5,7 @@ const baseUrls = {
dual: 'http://127.0.0.1:18183',
standaloneNew: 'http://127.0.0.1:18185',
standaloneApostrophe: 'http://127.0.0.1:18186',
standaloneAlpine: 'http://127.0.0.1:18187',
};
module.exports = {

View file

@ -23,6 +23,15 @@ test.describe('Runtime mode smoke coverage', () => {
await expect(page.locator('main > p.tagline')).toHaveText(defaultTagline);
});
test('Alpine standalone serves modern frontend settings', async ({ page, request }) => {
const settings = await request.get(`${baseUrls.standaloneAlpine}/settings.json`);
expect(settings.ok()).toBeTruthy();
await expect(settings.json()).resolves.toMatchObject({ telemetry_level: 'off', time_dl_max: 12 });
await page.goto(`${baseUrls.standaloneAlpine}/index-modern.html`);
await expect(modernStartButton(page)).toBeVisible();
});
test('backend exposes only local backend contract endpoints', async ({ request }) => {
for (const endpoint of ['/empty.php', '/garbage.php', '/getIP.php']) {
const response = await request.get(`${baseUrls.backend}${endpoint}`);

View file

@ -0,0 +1,35 @@
const { test, expect } = require("@playwright/test");
const staticRepositoryUrl = "http://127.0.0.1:18184";
const expectedAssets = [
"/index-modern.html",
"/settings.json",
"/server-list.json",
"/frontend/styling/index.css",
"/frontend/javascript/index.js",
"/frontend/images/logo.svg",
"/frontend/images/favicon.svg",
"/frontend/images/close-button.svg",
"/frontend/images/chevron.svg",
"/frontend/fonts/Inter-latin.woff2",
"/frontend/fonts/Inter-latin-ext.woff2",
"/frontend/images/background.jpeg",
"/speedtest.js",
"/speedtest_worker.js",
"/design-switch.js",
"/config.json",
"/images/icon-192.png"
];
test.describe("Unmodified repository static assets", () => {
test("serves every modern frontend dependency and returns 404 for a missing path", async ({ request }) => {
for (const path of expectedAssets) {
const response = await request.get(`${staticRepositoryUrl}${path}`);
expect(response.ok(), `${path} should return 2xx`).toBeTruthy();
}
const missing = await request.get(`${staticRepositoryUrl}/does-not-exist`);
expect(missing.status()).toBe(404);
});
});