diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0893d7fb57..83351a04a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Removed the deprecated `SymbolProfile` field from the activity interface
+- Refactored the language redirect of the root path from the static file serving configuration to a dedicated middleware
+
+### Fixed
+
+- Fixed the `RangeNotSatisfiableError` for requests with a `Range` header to the root path caused by the empty `index.html` placeholder
+- Fixed the unresolved template literal in the page title while the app is loading from the service worker cache
## 3.30.0 - 2026-07-19
diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts
index 4bdd50c9e8..bd76ef49b4 100644
--- a/apps/api/src/app/app.module.ts
+++ b/apps/api/src/app/app.module.ts
@@ -14,8 +14,6 @@ import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-g
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import {
BULL_BOARD_ROUTE,
- DEFAULT_LANGUAGE_CODE,
- SUPPORTED_LANGUAGE_CODES,
THROTTLE_DEFAULT_LIMIT,
THROTTLE_DEFAULT_TTL
} from '@ghostfolio/common/config';
@@ -143,29 +141,7 @@ import { UserModule } from './user/user.module';
'/api/*wildcard',
'/sitemap.xml'
],
- rootPath: join(__dirname, '..', 'client'),
- serveStaticOptions: {
- setHeaders: (res) => {
- if (res.req?.path === '/') {
- let languageCode = DEFAULT_LANGUAGE_CODE;
-
- try {
- const code = res.req.headers['accept-language']
- .split(',')[0]
- .split('-')[0];
-
- if (
- (SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(code)
- ) {
- languageCode = code;
- }
- } catch {}
-
- res.set('Location', `/${languageCode}`);
- res.statusCode = StatusCodes.MOVED_PERMANENTLY;
- }
- }
- }
+ rootPath: join(__dirname, '..', 'client')
}),
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'client', '.well-known'),
diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts
index 33ad032e98..77d571ea0b 100644
--- a/apps/api/src/main.ts
+++ b/apps/api/src/main.ts
@@ -1,3 +1,4 @@
+import { languageRedirectMiddleware } from '@ghostfolio/api/middlewares/language-redirect.middleware';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
BULL_BOARD_ROUTE,
@@ -100,6 +101,8 @@ async function bootstrap() {
});
}
+ app.use(languageRedirectMiddleware);
+
const configurationService = app.get(ConfigurationService);
const trustProxy = configurationService.get('TRUST_PROXY');
diff --git a/apps/api/src/middlewares/html-template.middleware.ts b/apps/api/src/middlewares/html-template.middleware.ts
index bdace34024..928a9f22c5 100644
--- a/apps/api/src/middlewares/html-template.middleware.ts
+++ b/apps/api/src/middlewares/html-template.middleware.ts
@@ -111,7 +111,13 @@ export class HtmlTemplateMiddleware implements NestMiddleware {
);
try {
- map[languageCode] = readFileSync(indexHtmlPath, 'utf8');
+ // Restore the interpolation token which the template replaces with a
+ // static fallback title to avoid showing an unresolved template
+ // literal when served without interpolation (e.g. by the service worker)
+ map[languageCode] = readFileSync(indexHtmlPath, 'utf8').replace(
+ /
.*?<\/title>/,
+ '${title}'
+ );
} catch {
this.logger.warn(
`Skipping language '${languageCode}': ${indexHtmlPath} not found`
diff --git a/apps/api/src/middlewares/language-redirect.middleware.ts b/apps/api/src/middlewares/language-redirect.middleware.ts
new file mode 100644
index 0000000000..5b6fac6c43
--- /dev/null
+++ b/apps/api/src/middlewares/language-redirect.middleware.ts
@@ -0,0 +1,37 @@
+import { environment } from '@ghostfolio/api/environments/environment';
+import {
+ DEFAULT_LANGUAGE_CODE,
+ SUPPORTED_LANGUAGE_CODES
+} from '@ghostfolio/common/config';
+
+import { NextFunction, Request, Response } from 'express';
+import { StatusCodes } from 'http-status-codes';
+
+export function languageRedirectMiddleware(
+ request: Request,
+ response: Response,
+ next: NextFunction
+) {
+ if (
+ !environment.production ||
+ request.path !== '/' ||
+ !['GET', 'HEAD'].includes(request.method)
+ ) {
+ return next();
+ }
+
+ let languageCode = DEFAULT_LANGUAGE_CODE;
+
+ try {
+ const code = request.headers['accept-language'].split(',')[0].split('-')[0];
+
+ if ((SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(code)) {
+ languageCode = code;
+ }
+ } catch {}
+
+ return response.redirect(
+ StatusCodes.MOVED_PERMANENTLY,
+ `/${languageCode}${request.url.slice(1)}`
+ );
+}
diff --git a/apps/client/project.json b/apps/client/project.json
index 9b0e8dafe2..c6367144ee 100644
--- a/apps/client/project.json
+++ b/apps/client/project.json
@@ -203,9 +203,6 @@
{
"command": "shx cp apps/client/src/assets/favicon.ico dist/apps/client"
},
- {
- "command": "shx cp apps/client/src/assets/index.html dist/apps/client"
- },
{
"command": "shx cp apps/client/src/assets/robots.txt dist/apps/client"
},
diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts
index 62bc78df16..0bed8111fb 100644
--- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts
+++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts
@@ -213,9 +213,16 @@ export class GfAdminOverviewComponent implements OnInit {
duration: this.couponDuration
};
+ const hasCopiedCouponCode = this.clipboard.copy(newCoupon.code);
+
const coupons = [...this.couponsDataSource.data, newCoupon];
- this.saveCoupons({ coupons, codeToCopy: newCoupon.code });
+ this.saveCoupons({
+ coupons,
+ snackBarMessage: hasCopiedCouponCode
+ ? '✅ ' + $localize`${newCoupon.code} has been copied to the clipboard`
+ : '✅ ' + $localize`Coupon ${newCoupon.code} has been created`
+ });
}
protected onChangeCouponDuration(aCouponDuration: StringValue) {
@@ -374,11 +381,11 @@ export class GfAdminOverviewComponent implements OnInit {
}
private saveCoupons({
- codeToCopy,
- coupons
+ coupons,
+ snackBarMessage
}: {
- codeToCopy?: string;
coupons: Coupon[];
+ snackBarMessage?: string;
}) {
this.dataService
.putAdminSetting(PROPERTY_COUPONS, {
@@ -388,14 +395,10 @@ export class GfAdminOverviewComponent implements OnInit {
.subscribe(() => {
this.couponsDataSource.data = coupons;
- if (codeToCopy) {
- this.clipboard.copy(codeToCopy);
-
- this.snackBar.open(
- '✅ ' + $localize`${codeToCopy} has been copied to the clipboard`,
- undefined,
- { duration: ms('3 seconds') }
- );
+ if (snackBarMessage) {
+ this.snackBar.open(snackBarMessage, undefined, {
+ duration: ms('3 seconds')
+ });
}
this.changeDetectorRef.markForCheck();
diff --git a/apps/client/src/assets/index.html b/apps/client/src/assets/index.html
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/apps/client/src/index.html b/apps/client/src/index.html
index c923e3e0c5..61953bca8d 100644
--- a/apps/client/src/index.html
+++ b/apps/client/src/index.html
@@ -1,7 +1,7 @@
- ${title}
+ Ghostfolio