Browse Source
Bugfix/eliminate unresolved page title and range not satisfiable error (#7387)
* Eliminate unresolved page title and RangeNotSatisfiableError
* Update changelog
pull/7371/head^2
Thomas Kaul
1 day ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with
60 additions and
30 deletions
-
CHANGELOG.md
-
apps/api/src/app/app.module.ts
-
apps/api/src/main.ts
-
apps/api/src/middlewares/html-template.middleware.ts
-
apps/api/src/middlewares/language-redirect.middleware.ts
-
apps/client/project.json
-
apps/client/src/assets/index.html
-
apps/client/src/index.html
|
|
|
@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. |
|
|
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
|
|
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
|
|
|
|
|
|
|
## Unreleased |
|
|
|
|
|
|
|
### Changed |
|
|
|
|
|
|
|
- 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 |
|
|
|
|
|
|
|
### Added |
|
|
|
|
|
|
|
@ -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'), |
|
|
|
|
|
|
|
@ -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'); |
|
|
|
|
|
|
|
@ -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>/, |
|
|
|
'<title>${title}</title>' |
|
|
|
); |
|
|
|
} catch { |
|
|
|
this.logger.warn( |
|
|
|
`Skipping language '${languageCode}': ${indexHtmlPath} not found` |
|
|
|
|
|
|
|
@ -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)}` |
|
|
|
); |
|
|
|
} |
|
|
|
@ -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" |
|
|
|
}, |
|
|
|
|
|
|
|
@ -1,7 +1,7 @@ |
|
|
|
<!doctype html> |
|
|
|
<html class="h-100 position-relative" lang="${languageCode}"> |
|
|
|
<head> |
|
|
|
<title>${title}</title> |
|
|
|
<title>Ghostfolio</title> |
|
|
|
<base href="/" /> |
|
|
|
<meta charset="utf-8" /> |
|
|
|
<meta content="yes" name="apple-mobile-web-app-capable" /> |
|
|
|
|