@bleedingdev/modern-js-create 3.2.0-ultramodern.12 → 3.2.0-ultramodern.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1139,6 +1139,18 @@ import { moduleFederationPlugin } from '@module-federation/modern-js-v3';
1139
1139
 
1140
1140
  const appId = '${app.id}';
1141
1141
  const port = Number(process.env['${app.portEnv}'] ?? ${app.port});
1142
+ const configuredSiteUrl = process.env['MODERN_PUBLIC_SITE_URL'];
1143
+ const hasConfiguredSiteUrl = typeof configuredSiteUrl === 'string' && configuredSiteUrl.length > 0;
1144
+ const isProductionBuild =
1145
+ process.env['NODE_ENV'] === 'production' || process.argv.includes('build');
1146
+
1147
+ if (isProductionBuild && !hasConfiguredSiteUrl) {
1148
+ throw new Error(
1149
+ 'MODERN_PUBLIC_SITE_URL must be set for production builds so canonical and hreflang URLs use the deployed origin.',
1150
+ );
1151
+ }
1152
+
1153
+ const siteUrl = hasConfiguredSiteUrl ? configuredSiteUrl : \`http://localhost:\${port}\`;
1142
1154
 
1143
1155
  export default defineConfig(
1144
1156
  presetUltramodern(
@@ -1154,6 +1166,7 @@ export default defineConfig(
1154
1166
  localeDetection: {
1155
1167
  fallbackLanguage: 'en',
1156
1168
  languages: ['en', 'cs'],
1169
+ localePathRedirect: true,
1157
1170
  },
1158
1171
  }),
1159
1172
  tanstackRouterPlugin(),
@@ -1166,6 +1179,11 @@ export default defineConfig(
1166
1179
  moduleFederationAppSSR: true,
1167
1180
  },
1168
1181
  },
1182
+ source: {
1183
+ globalVars: {
1184
+ ULTRAMODERN_SITE_URL: siteUrl,
1185
+ },
1186
+ },
1169
1187
  },
1170
1188
  {
1171
1189
  appId,
@@ -1330,32 +1348,103 @@ export default defineRuntimeConfig({
1330
1348
  });
1331
1349
  `;
1332
1350
  }
1351
+ function createLocalizedHeadComponent(includeLocationSuffix = false) {
1352
+ return `const fallbackLanguage = 'en';
1353
+ const supportedLanguages = ['en', 'cs'] as const;
1354
+ type SupportedLanguage = (typeof supportedLanguages)[number];
1355
+
1356
+ const isSupportedLanguage = (value: string): value is SupportedLanguage =>
1357
+ supportedLanguages.includes(value as SupportedLanguage);
1358
+
1359
+ const stripLanguagePrefix = (pathname: string) => {
1360
+ const segments = pathname.split('/').filter(Boolean);
1361
+ if (segments.length > 0 && isSupportedLanguage(segments[0] ?? '')) {
1362
+ segments.shift();
1363
+ }
1364
+ return \`/\${segments.join('/')}\`;
1365
+ };
1366
+
1367
+ const localizedPath = (pathname: string, language: SupportedLanguage) => {
1368
+ const pathWithoutLanguage = stripLanguagePrefix(pathname);
1369
+ return pathWithoutLanguage === '/' ? \`/\${language}\` : \`/\${language}\${pathWithoutLanguage}\`;
1370
+ };
1371
+
1372
+ const absoluteUrl = (pathname: string) => {
1373
+ const origin = ULTRAMODERN_SITE_URL.replace(/\\/+$/u, '');
1374
+ return \`\${origin}\${pathname}\`;
1375
+ };
1376
+ ${includeLocationSuffix ? `
1377
+ const locationSuffix = (location: { hash?: unknown; search?: unknown; searchStr?: unknown }) => {
1378
+ const { hash, search, searchStr } = location;
1379
+ let locationSearch = '';
1380
+ if (typeof searchStr === 'string') {
1381
+ locationSearch = searchStr;
1382
+ } else if (typeof search === 'string') {
1383
+ locationSearch = search;
1384
+ }
1385
+ const locationHash = typeof hash === 'string' ? hash : '';
1386
+ return \`\${locationSearch}\${locationHash}\`;
1387
+ };
1388
+ ` : ''}
1389
+ const LocalizedHead = () => {
1390
+ const { language } = useModernI18n();
1391
+ const location = useLocation();
1392
+ const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
1393
+ const canonicalPath = localizedPath(location.pathname, currentLanguage);
1394
+
1395
+ return (
1396
+ <Helmet>
1397
+ <link rel="canonical" href={absoluteUrl(canonicalPath)} />
1398
+ {supportedLanguages.map((code) => (
1399
+ <link
1400
+ href={absoluteUrl(localizedPath(location.pathname, code))}
1401
+ hrefLang={code}
1402
+ key={code}
1403
+ rel="alternate"
1404
+ />
1405
+ ))}
1406
+ <link
1407
+ href={absoluteUrl(localizedPath(location.pathname, fallbackLanguage))}
1408
+ hrefLang="x-default"
1409
+ rel="alternate"
1410
+ />
1411
+ </Helmet>
1412
+ );
1413
+ };
1414
+ `;
1415
+ }
1333
1416
  function createShellPage() {
1334
- return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
1417
+ return `import { Helmet } from '@modern-js/runtime/head';
1418
+ import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
1419
+ import { useLocation } from '@modern-js/runtime/tanstack-router';
1335
1420
  import { useTranslation } from 'react-i18next';
1336
1421
 
1337
1422
  const remotes = ['remote-commerce', 'remote-identity', 'remote-design-system'];
1338
1423
 
1424
+ ${createLocalizedHeadComponent(true)}
1339
1425
  export default function ShellHome() {
1340
1426
  const { t } = useTranslation();
1341
- const { changeLanguage, language } = useModernI18n();
1342
- const languageOptions = [
1343
- { code: 'en', label: t('language.en') },
1344
- { code: 'cs', label: t('language.cs') },
1345
- ];
1346
-
1427
+ const { language } = useModernI18n();
1428
+ const location = useLocation();
1429
+ const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
1430
+ const suffix = locationSuffix(location);
1431
+ const languageOptions = supportedLanguages.map((code) => ({
1432
+ code,
1433
+ href: \`\${localizedPath(location.pathname, code)}\${suffix}\`,
1434
+ label: t(\`language.\${code}\`),
1435
+ }));
1347
1436
  return (
1348
1437
  <main>
1438
+ <LocalizedHead />
1349
1439
  <nav aria-label={t('language.switcher')}>
1350
1440
  {languageOptions.map((option) => (
1351
- <button
1352
- disabled={language === option.code}
1441
+ <a
1442
+ aria-current={currentLanguage === option.code ? 'page' : undefined}
1443
+ href={option.href}
1353
1444
  key={option.code}
1354
- onClick={() => void changeLanguage(option.code)}
1355
- type="button"
1356
1445
  >
1357
1446
  {option.label}
1358
- </button>
1447
+ </a>
1359
1448
  ))}
1360
1449
  </nav>
1361
1450
  <h1>{t('shell.title')}</h1>
@@ -1371,13 +1460,18 @@ export default function ShellHome() {
1371
1460
  `;
1372
1461
  }
1373
1462
  function createRemotePage(app) {
1374
- return `import { useTranslation } from 'react-i18next';
1463
+ return `import { Helmet } from '@modern-js/runtime/head';
1464
+ import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
1465
+ import { useLocation } from '@modern-js/runtime/tanstack-router';
1466
+ import { useTranslation } from 'react-i18next';
1375
1467
 
1468
+ ${createLocalizedHeadComponent()}
1376
1469
  export default function ${toPascalCase(app.id)}Home() {
1377
1470
  const { t } = useTranslation();
1378
1471
 
1379
1472
  return (
1380
1473
  <main>
1474
+ <LocalizedHead />
1381
1475
  <h1>{t('remote.title')}</h1>
1382
1476
  <p data-mf-role="${app.kind}">{t('remote.domain')}</p>
1383
1477
  </main>
@@ -1837,14 +1931,14 @@ function createTemplateManifest(modernVersion, packageSource) {
1837
1931
  function writeApp(targetDir, scope, app, packageSource) {
1838
1932
  writeJson(targetDir, `${app.directory}/package.json`, createAppPackage(scope, app, packageSource));
1839
1933
  writeJson(targetDir, `${app.directory}/tsconfig.json`, createPackageTsConfig(app.directory));
1840
- writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n");
1934
+ writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n\ndeclare const ULTRAMODERN_SITE_URL: string;\n");
1841
1935
  writeFile(targetDir, `${app.directory}/modern.config.ts`, createAppModernConfig(app));
1842
1936
  writeFile(targetDir, `${app.directory}/src/modern.runtime.ts`, createAppRuntimeConfig());
1843
1937
  writeJson(targetDir, `${app.directory}/config/public/locales/en/translation.json`, createEnglishTranslations(app));
1844
1938
  writeJson(targetDir, `${app.directory}/config/public/locales/cs/translation.json`, createCzechTranslations(app));
1845
1939
  writeFile(targetDir, `${app.directory}/module-federation.config.ts`, 'shell' === app.kind ? createShellModuleFederationConfig() : createRemoteModuleFederationConfig(app));
1846
1940
  writeFile(targetDir, `${app.directory}/src/routes/layout.tsx`, createLayout(app.id));
1847
- writeFile(targetDir, `${app.directory}/src/routes/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
1941
+ writeFile(targetDir, `${app.directory}/src/routes/[lang]/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
1848
1942
  if ('vertical' === app.kind) {
1849
1943
  writeFile(targetDir, `${app.directory}/src/remote-entry.tsx`, createRemoteEntry(app));
1850
1944
  const widgetFile = 'remote-identity' === app.id ? 'identity-widget.tsx' : 'commerce-widget.tsx';
package/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
24
- "version": "3.2.0-ultramodern.12",
24
+ "version": "3.2.0-ultramodern.13",
25
25
  "types": "./dist/types/index.d.ts",
26
26
  "main": "./dist/index.js",
27
27
  "bin": {
@@ -41,7 +41,7 @@
41
41
  "@types/node": "^25.8.0",
42
42
  "@typescript/native-preview": "7.0.0-dev.20260516.1",
43
43
  "tsx": "^4.22.0",
44
- "@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.12"
44
+ "@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.13"
45
45
  },
46
46
  "publishConfig": {
47
47
  "registry": "https://registry.npmjs.org/",
@@ -54,6 +54,6 @@
54
54
  "start": "node ./dist/index.js"
55
55
  },
56
56
  "ultramodern": {
57
- "frameworkVersion": "3.2.0-ultramodern.12"
57
+ "frameworkVersion": "3.2.0-ultramodern.13"
58
58
  }
59
59
  }
@@ -14,6 +14,8 @@ This project is generated for Codex-first UltraModern.js work.
14
14
 
15
15
  Runtime i18n is enabled by default. Agents must put user-visible UI copy in `config/public/locales/<lang>/translation.json` and render it through `react-i18next` or `@modern-js/plugin-i18n/runtime`. Do not add hardcoded JSX text, `aria-label`, `title`, `alt`, or `placeholder` strings unless the value is a non-translatable technical token.
16
16
 
17
+ Routes are locale-prefixed by default through `localePathRedirect: true`. Keep localized pages under `src/routes/[lang]`, use links for language switching, and preserve canonical plus `hreflang` metadata. Production builds fail unless `MODERN_PUBLIC_SITE_URL` is set, so deployed canonical URLs always use the production origin.
18
+
17
19
  ## Private Skills
18
20
 
19
21
  Private orchestration skills are not vendored into this template. If you are authorized for `TechsioCZ/skills`, run:
@@ -12,6 +12,18 @@ const enableTelemetryExporters =
12
12
  process.env['MODERN_BASELINE_ENABLE_TELEMETRY_EXPORTERS'] !== 'false';
13
13
  const telemetryFailLoudStartup = process.env['MODERN_TELEMETRY_FAIL_LOUD_STARTUP'] !== 'false';
14
14
  const otlpEndpoint = process.env['MODERN_TELEMETRY_OTLP_ENDPOINT'];
15
+ const configuredSiteUrl = process.env['MODERN_PUBLIC_SITE_URL'];
16
+ const hasConfiguredSiteUrl = typeof configuredSiteUrl === 'string' && configuredSiteUrl.length > 0;
17
+ const isProductionBuild =
18
+ process.env['NODE_ENV'] === 'production' || process.argv.includes('build');
19
+
20
+ if (isProductionBuild && !hasConfiguredSiteUrl) {
21
+ throw new Error(
22
+ 'MODERN_PUBLIC_SITE_URL must be set for production builds so canonical and hreflang URLs use the deployed origin.',
23
+ );
24
+ }
25
+
26
+ const siteUrl = hasConfiguredSiteUrl ? configuredSiteUrl : 'http://localhost:8080';
15
27
  const victoriaMetricsEndpoint = process.env['MODERN_TELEMETRY_VICTORIA_ENDPOINT'];
16
28
 
17
29
  // https://bleedingdev.github.io/ultramodern.js/configure/app/usage.html
@@ -32,6 +44,7 @@ export default defineConfig(
32
44
  localeDetection: {
33
45
  fallbackLanguage: 'en',
34
46
  languages: ['en', 'cs'],
47
+ localePathRedirect: true,
35
48
  },
36
49
  }),
37
50
  {{#if isTanstackRouter}}
@@ -39,6 +52,11 @@ export default defineConfig(
39
52
  {{/if}}{{#if enableBff}}
40
53
  bffPlugin(),
41
54
  {{/if}} ],
55
+ source: {
56
+ globalVars: {
57
+ ULTRAMODERN_SITE_URL: siteUrl,
58
+ },
59
+ },
42
60
  },
43
61
  {
44
62
  appId,
@@ -17,6 +17,10 @@ const requiredTokens = [
17
17
  'enableBffRequestId',
18
18
  'enableTelemetryExporters',
19
19
  'i18nPlugin(',
20
+ 'localePathRedirect: true',
21
+ 'ULTRAMODERN_SITE_URL',
22
+ 'MODERN_PUBLIC_SITE_URL must be set for production builds',
23
+ 'globalVars',
20
24
  ];
21
25
  const missing = requiredTokens.filter((token) => !content.includes(token));
22
26
 
@@ -55,6 +59,8 @@ const requiredPaths = [
55
59
  'scripts/check-i18n-strings.mjs',
56
60
  'config/public/locales/en/translation.json',
57
61
  'config/public/locales/cs/translation.json',
62
+ 'src/modern-app-env.d.ts',
63
+ 'src/routes/[lang]/page.tsx',
58
64
  ];
59
65
  const manifestErrors = [];
60
66
 
@@ -65,6 +71,28 @@ for (const requiredPath of requiredPaths) {
65
71
  }
66
72
  }
67
73
 
74
+ if (fs.existsSync(path.resolve(process.cwd(), 'src/routes/page.tsx'))) {
75
+ console.error('src/routes/page.tsx must move under src/routes/[lang]/page.tsx');
76
+ process.exit(1);
77
+ }
78
+
79
+ const pageContent = fs.readFileSync(
80
+ path.resolve(process.cwd(), 'src/routes/[lang]/page.tsx'),
81
+ 'utf-8',
82
+ );
83
+ for (const token of [
84
+ 'rel="canonical"',
85
+ 'rel="alternate"',
86
+ 'hrefLang="x-default"',
87
+ 'localizedPath(',
88
+ '<a',
89
+ ]) {
90
+ if (!pageContent.includes(token)) {
91
+ console.error(`Localized route is missing ${token}`);
92
+ process.exit(1);
93
+ }
94
+ }
95
+
68
96
  if (templateManifest.schemaVersion !== 1) {
69
97
  manifestErrors.push('schemaVersion');
70
98
  }
@@ -1 +1,3 @@
1
1
  /// <reference types='@modern-js/app-tools/types' />
2
+
3
+ declare const ULTRAMODERN_SITE_URL: string;
@@ -1,19 +1,62 @@
1
1
  import { Helmet } from '@modern-js/runtime/head';
2
2
  import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
3
+ import { useLocation } from '@modern-js/runtime/{{routerImportPath}}';
3
4
  {{#if useEffectBff}}import effectBff from '@api/effect/index';
4
5
  import { Effect } from '@modern-js/plugin-bff/effect-client';
5
6
  import { useEffect, useState } from 'react';
6
7
  {{/if}}
7
8
  import { useTranslation } from 'react-i18next';
8
- import './index.css';
9
+ import '../index.css';
10
+
11
+ const fallbackLanguage = 'en';
12
+ const supportedLanguages = ['en', 'cs'] as const;
13
+ type SupportedLanguage = (typeof supportedLanguages)[number];
14
+
15
+ const isSupportedLanguage = (value: string): value is SupportedLanguage =>
16
+ supportedLanguages.includes(value as SupportedLanguage);
17
+
18
+ const stripLanguagePrefix = (pathname: string) => {
19
+ const segments = pathname.split('/').filter(Boolean);
20
+ if (segments.length > 0 && isSupportedLanguage(segments[0] ?? '')) {
21
+ segments.shift();
22
+ }
23
+ return `/${segments.join('/')}`;
24
+ };
25
+
26
+ const localizedPath = (pathname: string, language: SupportedLanguage) => {
27
+ const pathWithoutLanguage = stripLanguagePrefix(pathname);
28
+ return pathWithoutLanguage === '/' ? `/${language}` : `/${language}${pathWithoutLanguage}`;
29
+ };
30
+
31
+ const absoluteUrl = (pathname: string) => {
32
+ const origin = ULTRAMODERN_SITE_URL.replace(/\/+$/u, '');
33
+ return `${origin}${pathname}`;
34
+ };
35
+
36
+ const locationSuffix = (location: { hash?: unknown; search?: unknown; searchStr?: unknown }) => {
37
+ const { hash, search, searchStr } = location;
38
+ let locationSearch = '';
39
+ if (typeof searchStr === 'string') {
40
+ locationSearch = searchStr;
41
+ } else if (typeof search === 'string') {
42
+ locationSearch = search;
43
+ }
44
+ const locationHash = typeof hash === 'string' ? hash : '';
45
+ return `${locationSearch}${locationHash}`;
46
+ };
9
47
 
10
48
  const Index = () => {
11
49
  const { t } = useTranslation();
12
- const { changeLanguage, language } = useModernI18n();
13
- const languageOptions = [
14
- { code: 'en', label: t('home.language.en') },
15
- { code: 'cs', label: t('home.language.cs') },
16
- ];
50
+ const { language } = useModernI18n();
51
+ const location = useLocation();
52
+ const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
53
+ const canonicalPath = localizedPath(location.pathname, currentLanguage);
54
+ const suffix = locationSuffix(location);
55
+ const languageOptions = supportedLanguages.map((code) => ({
56
+ code,
57
+ href: `${localizedPath(location.pathname, code)}${suffix}`,
58
+ label: t(`home.language.${code}`),
59
+ }));
17
60
  {{#if useEffectBff}} const [effectMessage, setEffectMessage] = useState('loading...');
18
61
 
19
62
  useEffect(() => {
@@ -42,18 +85,31 @@ const Index = () => {
42
85
  type="image/x-icon"
43
86
  href="https://lf3-static.bytednsdoc.com/obj/eden-cn/uhbfnupenuhf/favicon.ico"
44
87
  />
88
+ <link rel="canonical" href={absoluteUrl(canonicalPath)} />
89
+ {supportedLanguages.map((code) => (
90
+ <link
91
+ href={absoluteUrl(localizedPath(location.pathname, code))}
92
+ hrefLang={code}
93
+ key={code}
94
+ rel="alternate"
95
+ />
96
+ ))}
97
+ <link
98
+ href={absoluteUrl(localizedPath(location.pathname, fallbackLanguage))}
99
+ hrefLang="x-default"
100
+ rel="alternate"
101
+ />
45
102
  </Helmet>
46
103
  <main>
47
104
  <nav className="language-switcher" aria-label={t('home.language.switcher')}>
48
105
  {languageOptions.map((option) => (
49
- <button
50
- disabled={language === option.code}
106
+ <a
107
+ aria-current={currentLanguage === option.code ? 'page' : undefined}
108
+ href={option.href}
51
109
  key={option.code}
52
- onClick={() => void changeLanguage(option.code)}
53
- type="button"
54
110
  >
55
111
  {option.label}
56
- </button>
112
+ </a>
57
113
  ))}
58
114
  </nav>
59
115
  <div className="title">
@@ -14,6 +14,8 @@ This workspace is generated as an agent-ready UltraModern.js SuperApp. Agents sh
14
14
 
15
15
  Runtime i18n is enabled by default for generated apps. Agents must put user-visible UI copy in each app's `config/public/locales/<lang>/translation.json` and render it through `react-i18next` or `@modern-js/plugin-i18n/runtime`. Do not add hardcoded JSX text, `aria-label`, `title`, `alt`, or `placeholder` strings unless the value is a non-translatable technical token.
16
16
 
17
+ Routes are locale-prefixed by default through `localePathRedirect: true`. Keep localized app pages under `src/routes/[lang]`, use links for language switching, and preserve canonical plus `hreflang` metadata. Production builds fail unless `MODERN_PUBLIC_SITE_URL` is set per deployed app, so canonical URLs always use the production origin.
18
+
17
19
  ## Required Skill Baseline
18
20
 
19
21
  Use these skills when the task touches the matching subsystem:
@@ -65,25 +65,33 @@ const requiredPaths = [
65
65
  'apps/shell-super-app/config/public/locales/cs/translation.json',
66
66
  'apps/shell-super-app/modern.config.ts',
67
67
  'apps/shell-super-app/module-federation.config.ts',
68
+ 'apps/shell-super-app/src/modern-app-env.d.ts',
68
69
  'apps/shell-super-app/src/modern.runtime.ts',
70
+ 'apps/shell-super-app/src/routes/[lang]/page.tsx',
69
71
  'apps/remotes/remote-commerce/package.json',
70
72
  'apps/remotes/remote-commerce/config/public/locales/en/translation.json',
71
73
  'apps/remotes/remote-commerce/config/public/locales/cs/translation.json',
72
74
  'apps/remotes/remote-commerce/modern.config.ts',
73
75
  'apps/remotes/remote-commerce/module-federation.config.ts',
76
+ 'apps/remotes/remote-commerce/src/modern-app-env.d.ts',
74
77
  'apps/remotes/remote-commerce/src/modern.runtime.ts',
78
+ 'apps/remotes/remote-commerce/src/routes/[lang]/page.tsx',
75
79
  'apps/remotes/remote-identity/package.json',
76
80
  'apps/remotes/remote-identity/config/public/locales/en/translation.json',
77
81
  'apps/remotes/remote-identity/config/public/locales/cs/translation.json',
78
82
  'apps/remotes/remote-identity/modern.config.ts',
79
83
  'apps/remotes/remote-identity/module-federation.config.ts',
84
+ 'apps/remotes/remote-identity/src/modern-app-env.d.ts',
80
85
  'apps/remotes/remote-identity/src/modern.runtime.ts',
86
+ 'apps/remotes/remote-identity/src/routes/[lang]/page.tsx',
81
87
  'apps/remotes/remote-design-system/package.json',
82
88
  'apps/remotes/remote-design-system/config/public/locales/en/translation.json',
83
89
  'apps/remotes/remote-design-system/config/public/locales/cs/translation.json',
84
90
  'apps/remotes/remote-design-system/modern.config.ts',
85
91
  'apps/remotes/remote-design-system/module-federation.config.ts',
92
+ 'apps/remotes/remote-design-system/src/modern-app-env.d.ts',
86
93
  'apps/remotes/remote-design-system/src/modern.runtime.ts',
94
+ 'apps/remotes/remote-design-system/src/routes/[lang]/page.tsx',
87
95
  'services/service-recommendations-effect/package.json',
88
96
  'services/service-recommendations-effect/modern.config.ts',
89
97
  'services/service-recommendations-effect/api/effect/index.ts',
@@ -97,6 +105,18 @@ for (const requiredPath of requiredPaths) {
97
105
  assertExists(requiredPath);
98
106
  }
99
107
 
108
+ for (const appDirectory of [
109
+ 'apps/shell-super-app',
110
+ 'apps/remotes/remote-commerce',
111
+ 'apps/remotes/remote-identity',
112
+ 'apps/remotes/remote-design-system',
113
+ ]) {
114
+ assert(
115
+ !fs.existsSync(path.join(root, appDirectory, 'src/routes/page.tsx')),
116
+ `${appDirectory} must use src/routes/[lang]/page.tsx`,
117
+ );
118
+ }
119
+
100
120
  const rootPackage = readJson('package.json');
101
121
  const packageSource = readJson('.modernjs/ultramodern-package-source.json');
102
122
  const skillsLock = readJson('.agents/skills-lock.json');
@@ -286,6 +306,12 @@ for (const configPath of [
286
306
  const config = readText(configPath);
287
307
  assert(config.includes('presetUltramodern('), `${configPath} must use presetUltramodern`);
288
308
  assert(config.includes('i18nPlugin('), `${configPath} must enable plugin-i18n`);
309
+ assert(config.includes('localePathRedirect: true'), `${configPath} must prefix localized URLs`);
310
+ assert(config.includes('ULTRAMODERN_SITE_URL'), `${configPath} must expose site URL metadata`);
311
+ assert(
312
+ config.includes('MODERN_PUBLIC_SITE_URL must be set for production builds'),
313
+ `${configPath} must require MODERN_PUBLIC_SITE_URL for production builds`,
314
+ );
289
315
  assert(config.includes('tanstackRouterPlugin()'), `${configPath} must enable plugin-tanstack`);
290
316
  assert(
291
317
  config.includes('moduleFederationPlugin()'),
@@ -293,6 +319,19 @@ for (const configPath of [
293
319
  );
294
320
  }
295
321
 
322
+ for (const routePath of [
323
+ 'apps/shell-super-app/src/routes/[lang]/page.tsx',
324
+ 'apps/remotes/remote-commerce/src/routes/[lang]/page.tsx',
325
+ 'apps/remotes/remote-identity/src/routes/[lang]/page.tsx',
326
+ 'apps/remotes/remote-design-system/src/routes/[lang]/page.tsx',
327
+ ]) {
328
+ const route = readText(routePath);
329
+ assert(route.includes('rel="canonical"'), `${routePath} must emit canonical metadata`);
330
+ assert(route.includes('rel="alternate"'), `${routePath} must emit alternate locale metadata`);
331
+ assert(route.includes('hrefLang="x-default"'), `${routePath} must emit x-default metadata`);
332
+ assert(route.includes('localizedPath('), `${routePath} must build localized URLs`);
333
+ }
334
+
296
335
  const shellMf = readText('apps/shell-super-app/module-federation.config.ts');
297
336
  assert(shellMf.includes("name: 'shellSuperApp'"), 'Shell MF config must name the shell');
298
337
  assert(