Feature/add endpoint to localize site.webmanifest (#4450)

* Add endpoint to localize site.webmanifest

* Refactor rootUrl

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
This commit is contained in:
Ronnie Alsop 2025-03-22 01:36:45 -07:00 committed by GitHub
parent 198f73db00
commit a9c3224856
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 96 additions and 28 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added support for filtering in the _Copy AI prompt to clipboard_ actions on the analysis page (experimental) - Added support for filtering in the _Copy AI prompt to clipboard_ actions on the analysis page (experimental)
- Added an endpoint to localize the `site.webmanifest`
- Added the _Storybook_ path to the `sitemap.xml` file - Added the _Storybook_ path to the `sitemap.xml` file
### Changed ### Changed

View File

@ -32,6 +32,7 @@ import { AuthModule } from './auth/auth.module';
import { CacheModule } from './cache/cache.module'; import { CacheModule } from './cache/cache.module';
import { AiModule } from './endpoints/ai/ai.module'; import { AiModule } from './endpoints/ai/ai.module';
import { ApiKeysModule } from './endpoints/api-keys/api-keys.module'; import { ApiKeysModule } from './endpoints/api-keys/api-keys.module';
import { AssetsModule } from './endpoints/assets/assets.module';
import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module'; import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module';
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module'; import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
import { MarketDataModule } from './endpoints/market-data/market-data.module'; import { MarketDataModule } from './endpoints/market-data/market-data.module';
@ -61,6 +62,7 @@ import { UserModule } from './user/user.module';
AiModule, AiModule,
ApiKeysModule, ApiKeysModule,
AssetModule, AssetModule,
AssetsModule,
AuthDeviceModule, AuthDeviceModule,
AuthModule, AuthModule,
BenchmarksModule, BenchmarksModule,

View File

@ -0,0 +1,46 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { interpolate } from '@ghostfolio/common/helper';
import {
Controller,
Get,
Param,
Res,
Version,
VERSION_NEUTRAL
} from '@nestjs/common';
import { Response } from 'express';
import { readFileSync } from 'fs';
import { join } from 'path';
@Controller('assets')
export class AssetsController {
private webManifest = '';
public constructor(
public readonly configurationService: ConfigurationService
) {
try {
this.webManifest = readFileSync(
join(__dirname, 'assets', 'site.webmanifest'),
'utf8'
);
} catch {}
}
@Get('/:languageCode/site.webmanifest')
@Version(VERSION_NEUTRAL)
public getWebManifest(
@Param('languageCode') languageCode: string,
@Res() response: Response
): void {
const rootUrl = this.configurationService.get('ROOT_URL');
const webManifest = interpolate(this.webManifest, {
languageCode,
rootUrl
});
response.setHeader('Content-Type', 'application/json');
response.send(webManifest);
}
}

View File

@ -0,0 +1,11 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { Module } from '@nestjs/common';
import { AssetsController } from './assets.controller';
@Module({
controllers: [AssetsController],
providers: [ConfigurationService]
})
export class AssetsModule {}

View File

@ -9,8 +9,8 @@ import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools'
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common'; import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { Response } from 'express'; import { Response } from 'express';
import * as fs from 'fs'; import { readFileSync } from 'fs';
import * as path from 'path'; import { join } from 'path';
@Controller('sitemap.xml') @Controller('sitemap.xml')
export class SitemapController { export class SitemapController {
@ -20,8 +20,8 @@ export class SitemapController {
private readonly configurationService: ConfigurationService private readonly configurationService: ConfigurationService
) { ) {
try { try {
this.sitemapXml = fs.readFileSync( this.sitemapXml = readFileSync(
path.join(__dirname, 'assets', 'sitemap.xml'), join(__dirname, 'assets', 'sitemap.xml'),
'utf8' 'utf8'
); );
} catch {} } catch {}

View File

@ -25,7 +25,7 @@
"name": "Ghostfolio", "name": "Ghostfolio",
"orientation": "portrait", "orientation": "portrait",
"short_name": "Ghostfolio", "short_name": "Ghostfolio",
"start_url": "/en/", "start_url": "/${languageCode}/",
"theme_color": "#FFFFFF", "theme_color": "#FFFFFF",
"url": "https://ghostfol.io" "url": "${rootUrl}"
} }

View File

@ -1,4 +1,7 @@
import { DEFAULT_HOST, DEFAULT_PORT } from '@ghostfolio/common/config';
export const environment = { export const environment = {
production: true, production: true,
rootUrl: `http://${DEFAULT_HOST}:${DEFAULT_PORT}`,
version: `${require('../../../../package.json').version}` version: `${require('../../../../package.json').version}`
}; };

View File

@ -1,4 +1,7 @@
import { DEFAULT_HOST } from '@ghostfolio/common/config';
export const environment = { export const environment = {
production: false, production: false,
rootUrl: `https://${DEFAULT_HOST}:4200`,
version: 'dev' version: 'dev'
}; };

View File

@ -1,4 +1,8 @@
import { STORYBOOK_PATH } from '@ghostfolio/common/config'; import {
DEFAULT_HOST,
DEFAULT_PORT,
STORYBOOK_PATH
} from '@ghostfolio/common/config';
import { import {
Logger, Logger,
@ -75,8 +79,8 @@ async function bootstrap() {
app.use(HtmlTemplateMiddleware); app.use(HtmlTemplateMiddleware);
const HOST = configService.get<string>('HOST') || '0.0.0.0'; const HOST = configService.get<string>('HOST') || DEFAULT_HOST;
const PORT = configService.get<number>('PORT') || 3333; const PORT = configService.get<number>('PORT') || DEFAULT_PORT;
await app.listen(PORT, HOST, () => { await app.listen(PORT, HOST, () => {
logLogo(); logLogo();

View File

@ -2,7 +2,6 @@ import { environment } from '@ghostfolio/api/environments/environment';
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
import { import {
DEFAULT_LANGUAGE_CODE, DEFAULT_LANGUAGE_CODE,
DEFAULT_ROOT_URL,
STORYBOOK_PATH, STORYBOOK_PATH,
SUPPORTED_LANGUAGE_CODES SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
@ -126,7 +125,7 @@ export const HtmlTemplateMiddleware = async (
} }
const currentDate = format(new Date(), DATE_FORMAT); const currentDate = format(new Date(), DATE_FORMAT);
const rootUrl = process.env.ROOT_URL || DEFAULT_ROOT_URL; const rootUrl = process.env.ROOT_URL || environment.rootUrl;
if ( if (
path.startsWith('/api/') || path.startsWith('/api/') ||

View File

@ -1,11 +1,13 @@
import { environment } from '@ghostfolio/api/environments/environment';
import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface'; import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface';
import { import {
CACHE_TTL_NO_CACHE, CACHE_TTL_NO_CACHE,
DEFAULT_HOST,
DEFAULT_PORT,
DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY, DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY,
DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY, DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY,
DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY, DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY,
DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT, DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT
DEFAULT_ROOT_URL
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -49,11 +51,11 @@ export class ConfigurationService {
GOOGLE_SHEETS_ACCOUNT: str({ default: '' }), GOOGLE_SHEETS_ACCOUNT: str({ default: '' }),
GOOGLE_SHEETS_ID: str({ default: '' }), GOOGLE_SHEETS_ID: str({ default: '' }),
GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }), GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }),
HOST: host({ default: '0.0.0.0' }), HOST: host({ default: DEFAULT_HOST }),
JWT_SECRET_KEY: str({}), JWT_SECRET_KEY: str({}),
MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }), MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
MAX_CHART_ITEMS: num({ default: 365 }), MAX_CHART_ITEMS: num({ default: 365 }),
PORT: port({ default: 3333 }), PORT: port({ default: DEFAULT_PORT }),
PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: num({ PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: num({
default: DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY default: DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY
}), }),
@ -71,7 +73,9 @@ export class ConfigurationService {
REDIS_PASSWORD: str({ default: '' }), REDIS_PASSWORD: str({ default: '' }),
REDIS_PORT: port({ default: 6379 }), REDIS_PORT: port({ default: 6379 }),
REQUEST_TIMEOUT: num({ default: ms('3 seconds') }), REQUEST_TIMEOUT: num({ default: ms('3 seconds') }),
ROOT_URL: url({ default: DEFAULT_ROOT_URL }), ROOT_URL: url({
default: environment.rootUrl
}),
STRIPE_PUBLIC_KEY: str({ default: '' }), STRIPE_PUBLIC_KEY: str({ default: '' }),
STRIPE_SECRET_KEY: str({ default: '' }), STRIPE_SECRET_KEY: str({ default: '' }),
TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }), TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }),

View File

@ -6,13 +6,7 @@
"name": "app", "name": "app",
"installMode": "prefetch", "installMode": "prefetch",
"resources": { "resources": {
"files": [ "files": ["/favicon.ico", "/index.html", "/*.css", "/*.js"]
"/favicon.ico",
"/index.html",
"/assets/site.webmanifest",
"/*.css",
"/*.js"
]
} }
}, },
{ {

View File

@ -146,9 +146,6 @@
{ {
"command": "shx cp apps/client/src/assets/robots.txt dist/apps/client" "command": "shx cp apps/client/src/assets/robots.txt dist/apps/client"
}, },
{
"command": "shx cp apps/client/src/assets/site.webmanifest dist/apps/client"
},
{ {
"command": "shx cp node_modules/ionicons/dist/index.js dist/apps/client" "command": "shx cp node_modules/ionicons/dist/index.js dist/apps/client"
}, },

View File

@ -45,7 +45,10 @@
sizes="16x16" sizes="16x16"
type="image/png" type="image/png"
/> />
<link href="../assets/site.webmanifest" rel="manifest" /> <link
href="../api/assets/${languageCode}/site.webmanifest"
rel="manifest"
/>
</head> </head>
<body> <body>
<gf-root></gf-root> <gf-root></gf-root>

View File

@ -48,13 +48,14 @@ export const PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW =
export const DEFAULT_CURRENCY = 'USD'; export const DEFAULT_CURRENCY = 'USD';
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
export const DEFAULT_HOST = '0.0.0.0';
export const DEFAULT_LANGUAGE_CODE = 'en'; export const DEFAULT_LANGUAGE_CODE = 'en';
export const DEFAULT_PAGE_SIZE = 50; export const DEFAULT_PAGE_SIZE = 50;
export const DEFAULT_PORT = 3333;
export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1;
export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1;
export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1;
export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000;
export const DEFAULT_ROOT_URL = 'https://localhost:4200';
// USX is handled separately // USX is handled separately
export const DERIVED_CURRENCIES = [ export const DERIVED_CURRENCIES = [