Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/agenstra/frontend-agent-console/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { dirname, join, resolve } from 'path';
import { fileURLToPath } from 'url';

import {
buildRootRedirectLocation,
createCachedStaticFile,
createMemoryStaticMiddleware,
createSecurityHeadersMiddleware,
Expand Down Expand Up @@ -356,6 +357,14 @@ app.get('*', (req, res) => {
}

if (!getLocaleFromPath(req)) {
const requestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);

if (requestUrl.pathname === '/') {
res.redirect(301, buildRootRedirectLocation(locale, '/', requestUrl.search));

return;
}

res.redirect(302, `/${locale}${req.url}`);

return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { startDelegatingServerFromImportMetaUrl } from '@forepath/shared/fronten
startDelegatingServerFromImportMetaUrl(import.meta.url, {
availableLocales: ['en', 'de'],
defaultLocale: process.env['DEFAULT_LOCALE'] || 'en',
rootRedirectPath: '/',
});

export default undefined;
9 changes: 9 additions & 0 deletions apps/decabill/frontend-billing-console/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { dirname, join, resolve } from 'path';
import { fileURLToPath } from 'url';

import {
buildRootRedirectLocation,
createCachedStaticFile,
createMemoryStaticMiddleware,
createSecurityHeadersMiddleware,
Expand Down Expand Up @@ -356,6 +357,14 @@ app.get('*', (req, res) => {
}

if (!getLocaleFromPath(req)) {
const requestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);

if (requestUrl.pathname === '/') {
res.redirect(301, buildRootRedirectLocation(locale, '/', requestUrl.search));

return;
}

res.redirect(302, `/${locale}${req.url}`);

return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { startDelegatingServerFromImportMetaUrl } from '@forepath/shared/fronten
startDelegatingServerFromImportMetaUrl(import.meta.url, {
availableLocales: ['en', 'de'],
defaultLocale: process.env['DEFAULT_LOCALE'] || 'en',
rootRedirectPath: '/',
});

export default undefined;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ startDelegatingServerFromImportMetaUrl(import.meta.url, {
availableLocales: ['en', 'de'],
defaultLocale: process.env['DEFAULT_LOCALE'] || 'en',
shouldBypassStatic,
rootRedirectPath: '/',
});

export default undefined;
1 change: 1 addition & 0 deletions apps/shared/frontend-docs/src/delegating-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { startDelegatingServerFromImportMetaUrl } from '@forepath/shared/fronten
startDelegatingServerFromImportMetaUrl(import.meta.url, {
availableLocales: ['en', 'de'],
defaultLocale: process.env['DEFAULT_LOCALE'] || 'en',
rootRedirectPath: '/docs',
});

export default undefined;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Delegating server entry — import from
// `@forepath/shared/frontend/util-express-server/delegating-server` (no Angular SSR).
export {
buildRootRedirectLocation,
createDelegatingServer,
resolveLocaleFromRequest,
resolveLocalizedStaticFilePath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {
} from './lib/security-headers';
export { buildSsrAllowedHosts } from './lib/ssr-allowed-hosts';
export { resolveLocalizedBrowserDistFolder, stripLocalePrefixFromPath } from './lib/localized-browser-dist';
export { buildRootRedirectLocation } from './lib/create-delegating-server';
export {
buildStaticCacheHeaders,
buildStaticCacheHeadersFor304,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { join } from 'node:path';
import { pathToFileURL } from 'node:url';

import {
buildRootRedirectLocation,
createDelegatingServer,
defaultLoadLocaleServerModule,
resolveLocaleFromRequest,
Expand Down Expand Up @@ -88,6 +89,11 @@ describe('create-delegating-server', () => {
writeFileSync(join(serverRoot, 'browser', 'en', 'about.html'), '<html>about</html>');
mkdirSync(join(serverRoot, 'browser', 'en', 'pricing'), { recursive: true });
writeFileSync(join(serverRoot, 'browser', 'en', 'pricing', 'index.html'), '<html>pricing</html>');
mkdirSync(join(serverRoot, 'browser', 'en', 'docs'), { recursive: true });
writeFileSync(join(serverRoot, 'browser', 'en', 'docs', 'index.html'), '<html>en-docs</html>');
mkdirSync(join(serverRoot, 'browser', 'de', 'docs'), { recursive: true });
writeFileSync(join(serverRoot, 'browser', 'de', 'index.html'), '<html>de-home</html>');
writeFileSync(join(serverRoot, 'browser', 'de', 'docs', 'index.html'), '<html>de-docs</html>');
mkdirSync(join(serverRoot, 'en'), { recursive: true });
mkdirSync(join(serverRoot, 'de'), { recursive: true });
writeFileSync(join(serverRoot, 'en', 'server.mjs'), '// placeholder for existsSync\n');
Expand Down Expand Up @@ -127,6 +133,15 @@ describe('create-delegating-server', () => {
});
});

describe('buildRootRedirectLocation', () => {
it('normalizes landing and docs redirect targets', () => {
expect(buildRootRedirectLocation('en', '/')).toBe('/en/');
expect(buildRootRedirectLocation('de', '/docs')).toBe('/de/docs');
expect(buildRootRedirectLocation('en', '/docs', '?utm=x')).toBe('/en/docs?utm=x');
expect(buildRootRedirectLocation('en', '/', '?utm=x')).toBe('/en/?utm=x');
});
});

describe('defaultLoadLocaleServerModule', () => {
it('dynamically imports a locale server module path', async () => {
const loaded = await defaultLoadLocaleServerModule(join(serverRoot, 'en', 'server.mjs'));
Expand Down Expand Up @@ -474,6 +489,86 @@ describe('create-delegating-server', () => {
}
}
});

it('301-redirects bare / to /{locale}/docs when rootRedirectPath is /docs', async () => {
const handle = createDelegatingServer({
serverRoot,
availableLocales: ['en', 'de'],
defaultLocale: 'en',
port: 0,
rootRedirectPath: '/docs',
loadLocaleServerModule: createLocaleLoader({ en: enHandler, de: deHandler }),
});

await handle.listen();
const address = handle.server.address();
const port = typeof address === 'object' && address ? address.port : 0;

try {
const root = await httpGet(port, '/');
expect(root.status).toBe(301);
expect(root.headers['location']).toBe('/en/docs');
expect(root.body).toBe('');

const withQuery = await httpGet(port, '/?utm=x');
expect(withQuery.status).toBe(301);
expect(withQuery.headers['location']).toBe('/en/docs?utm=x');

const deRoot = await httpGet(port, '/', { 'accept-language': 'de' });
expect(deRoot.status).toBe(301);
expect(deRoot.headers['location']).toBe('/de/docs');

const docsHome = await httpGet(port, '/en/docs');
expect(docsHome.status).toBe(200);
expect(docsHome.body).toContain('en-docs');

const about = await httpGet(port, '/en/about');
expect(about.status).toBe(200);
expect(about.body).toContain('about');
} finally {
await closeServer(handle.server);
}
});

it('301-redirects bare / to /{locale}/ when rootRedirectPath is /', async () => {
const handle = createDelegatingServer({
serverRoot,
availableLocales: ['en', 'de'],
defaultLocale: 'en',
port: 0,
rootRedirectPath: '/',
loadLocaleServerModule: createLocaleLoader({ en: enHandler, de: deHandler }),
});

await handle.listen();
const address = handle.server.address();
const port = typeof address === 'object' && address ? address.port : 0;

try {
const root = await httpGet(port, '/');
expect(root.status).toBe(301);
expect(root.headers['location']).toBe('/en/');
expect(root.body).toBe('');

const withQuery = await httpGet(port, '/?utm=x');
expect(withQuery.status).toBe(301);
expect(withQuery.headers['location']).toBe('/en/?utm=x');

const deRoot = await httpGet(port, '/', { 'accept-language': 'de' });
expect(deRoot.status).toBe(301);
expect(deRoot.headers['location']).toBe('/de/');

const localeHome = await httpGet(port, '/en/');
expect(localeHome.status).toBe(200);
expect(localeHome.body).toContain('en-home');

const deHome = await httpGet(port, '/de/');
expect(deHome.status).toBe(200);
expect(deHome.body).toContain('de-home');
} finally {
await closeServer(handle.server);
}
});
});

describe('startDelegatingServerFromImportMetaUrl', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,32 @@ export interface CreateDelegatingServerOptions {
defaultLocale?: string;
port?: number | string;
shouldBypassStatic?: (pathname: string) => boolean;
/**
* When set, `GET /` returns HTTP 301 to `/{locale}{rootRedirectPath}` instead of
* serving the locale `index.html` (docs: `'/docs'`, landings: `'/'`).
* Exact pathname `/` only; unset preserves current behavior.
*/
rootRedirectPath?: string;
/**
* Optional override for loading locale `server.mjs` modules (tests).
* Defaults to a dynamic `import()` of the file URL.
*/
loadLocaleServerModule?: LocaleServerModuleLoader;
}

/**
* Builds `Location` for a bare-host root redirect (`/` → `/{locale}/…`).
* `rootRedirectPath: '/'` yields `/{locale}/`; `'/docs'` yields `/{locale}/docs`.
*/
export function buildRootRedirectLocation(locale: string, rootRedirectPath: string, search = ''): string {
const path =
rootRedirectPath === '/'
? `/${locale}/`
: `/${locale}${rootRedirectPath.startsWith('/') ? rootRedirectPath : `/${rootRedirectPath}`}`;

return `${path}${search}`;
}

export interface DelegatingServerHandle {
server: Server;
listen: () => Promise<void>;
Expand Down Expand Up @@ -197,7 +216,7 @@ export function createDelegatingServer(options: CreateDelegatingServerOptions):
const availableLocales = options.availableLocales ?? ['en', 'de'];
const defaultLocale = options.defaultLocale ?? 'en';
const port = options.port ?? process.env['PORT'] ?? 4000;
const { serverRoot, shouldBypassStatic } = options;
const { serverRoot, shouldBypassStatic, rootRedirectPath } = options;
const loadLocaleServerModule = options.loadLocaleServerModule ?? defaultLoadLocaleServerModule;
let localeServers = new Map<string, LocaleExpressHandler>();

Expand All @@ -212,6 +231,17 @@ export function createDelegatingServer(options: CreateDelegatingServerOptions):
return;
}

if (requestUrl.pathname === '/' && rootRedirectPath) {
const redirectLocale = resolveLocaleFromRequest(req, availableLocales, defaultLocale);

res.writeHead(301, {
Location: buildRootRedirectLocation(redirectLocale, rootRedirectPath, requestUrl.search),
});
res.end();

return;
}

const locale = resolveLocaleFromRequest(req, availableLocales, defaultLocale);
const localeServer = localeServers.get(locale);

Expand Down
Loading