@vmz/vmz 0.0.4 → 0.1.0

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.
@@ -9,6 +9,7 @@ import path from 'node:path';
9
9
  import { pathToFileURL } from 'node:url';
10
10
  import { emitCdnPolicy } from './cdn-policy.js';
11
11
  import { emitContentAddressedAssets } from './content-addressed-assets.js';
12
+ import { absoluteUrl, buildLocalePageMeta, localizeBodyLinks } from './locale-router.js';
12
13
  export const STATIC_DELIVERY_MANIFEST_SCHEMA = 'vmz.static.delivery_manifest.v0';
13
14
  /**
14
15
  * @param {string} distDir
@@ -104,29 +105,46 @@ export async function emitWebStatic(distDir, opts = {}) {
104
105
  continue;
105
106
  bodyHtml = await renderToString(Layout, {}, { slotHtml: bodyHtml });
106
107
  }
107
- const htmlPath = htmlPathForRoute(pattern);
108
- const absHtml = path.join(distDir, htmlPath);
109
- fs.mkdirSync(path.dirname(absHtml), { recursive: true });
110
- const html = wrapDocument({
111
- bodyHtml,
108
+ const localeArt = loadLocaleArtifact(distDir);
109
+ // Locale artifact routeId is chunkId (`pages/about`), not the page class name.
110
+ const localeWrites = expandLocaleStaticGenerations({
111
+ localeArt,
112
+ routeId: page.chunkId,
112
113
  chunkId: page.chunkId,
113
- layoutChain,
114
- props,
115
- meta,
116
- cssEntry: readCssEntry(distDir),
117
- });
118
- fs.writeFileSync(absHtml, html, 'utf8');
119
- generations.push({
120
- routeId,
121
- path: pattern,
122
- chunkId: page.chunkId,
123
- htmlPath: htmlPath.replaceAll('\\', '/'),
124
- classification: 'Static',
125
- title: meta.title,
126
- description: meta.description,
127
- canonical: meta.canonical,
128
- robots: meta.robots,
114
+ pattern,
115
+ origin,
116
+ baseMeta: meta,
129
117
  });
118
+ for (const gen of localeWrites) {
119
+ const absHtml = path.join(distDir, gen.htmlPath);
120
+ fs.mkdirSync(path.dirname(absHtml), { recursive: true });
121
+ // Each LocaleId HTML must retain locale on same-app Links (realization authority).
122
+ const localizedBody = gen.localeId && localeArt
123
+ ? localizeBodyLinks(bodyHtml, gen.localeId, localeArt)
124
+ : bodyHtml;
125
+ const html = wrapDocument({
126
+ bodyHtml: localizedBody,
127
+ chunkId: page.chunkId,
128
+ layoutChain,
129
+ props,
130
+ meta: gen.meta,
131
+ cssEntry: readCssEntry(distDir),
132
+ });
133
+ fs.writeFileSync(absHtml, html, 'utf8');
134
+ generations.push({
135
+ routeId,
136
+ path: gen.path,
137
+ chunkId: page.chunkId,
138
+ htmlPath: gen.htmlPath.replaceAll('\\', '/'),
139
+ classification: 'Static',
140
+ title: gen.meta.title,
141
+ description: meta.description,
142
+ canonical: gen.meta.canonical,
143
+ robots: gen.meta.robots,
144
+ localeId: gen.localeId || null,
145
+ alternates: Array.isArray(gen.meta.alternates) ? gen.meta.alternates : [],
146
+ });
147
+ }
130
148
  }
131
149
  const notFoundHtml = wrapDocument({
132
150
  bodyHtml: '<main><h1>Not Found</h1><p>route-static-404</p></main>',
@@ -174,11 +192,13 @@ export async function emitWebStatic(distDir, opts = {}) {
174
192
  chunkId: g.chunkId,
175
193
  htmlPath: g.htmlPath,
176
194
  classification: g.classification,
195
+ localeId: g.localeId || null,
177
196
  seo: {
178
197
  title: g.title,
179
198
  description: g.description,
180
199
  canonical: g.canonical,
181
200
  robots: g.robots,
201
+ alternates: g.alternates || [],
182
202
  },
183
203
  })),
184
204
  skipped,
@@ -376,7 +396,94 @@ async function resolvePageMeta(Page, ctx) {
376
396
  const canonical = String(raw.canonical || `${ctx.origin}${ctx.pathname === '/' ? '/' : ctx.pathname}`);
377
397
  const robots = String(raw.robots || 'index,follow');
378
398
  const lang = String(raw.lang || 'en');
379
- return { title, description, canonical, robots, lang };
399
+ return { title, description, canonical, robots, lang, alternates: [] };
400
+ }
401
+ /**
402
+ * @param {string} distDir
403
+ */
404
+ function loadLocaleArtifact(distDir) {
405
+ const p = path.join(distDir, '_vmz', 'locale-route-realization.json');
406
+ if (!fs.existsSync(p))
407
+ return null;
408
+ try {
409
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
410
+ }
411
+ catch {
412
+ return null;
413
+ }
414
+ }
415
+ /**
416
+ * Expand one Static route across LocaleId realizations (hreflang seed + prefixed HTML).
417
+ * @param {{
418
+ * localeArt: any,
419
+ * routeId: string,
420
+ * chunkId: string,
421
+ * pattern: string,
422
+ * origin: string,
423
+ * baseMeta: { title: string, description: string, canonical: string, robots: string, lang: string, alternates?: any[] },
424
+ * }} input
425
+ */
426
+ function expandLocaleStaticGenerations(input) {
427
+ const { localeArt, routeId, pattern, origin, baseMeta } = input;
428
+ if (!localeArt?.realizations?.length) {
429
+ return [
430
+ {
431
+ path: pattern,
432
+ htmlPath: htmlPathForRoute(pattern),
433
+ localeId: null,
434
+ meta: baseMeta,
435
+ },
436
+ ];
437
+ }
438
+ const locales = (localeArt.locales || []).map((l) => l.id);
439
+ const directions = Object.fromEntries((localeArt.locales || []).map((l) => [l.id, l.direction || 'ltr']));
440
+ const defaultLocale = localeArt.defaultLocale || locales[0];
441
+ const forRoute = (localeArt.realizations || []).filter((r) => r.routeId === routeId ||
442
+ r.routeId === input.chunkId ||
443
+ r.pathPattern === pattern ||
444
+ (r.path === pattern && !r.prefixed));
445
+ /** @type {any[]} */
446
+ const out = [];
447
+ for (const loc of locales) {
448
+ const hit = forRoute.find((r) => r.localeId === loc);
449
+ if (!hit)
450
+ continue;
451
+ const built = buildLocalePageMeta({
452
+ routeId,
453
+ localeId: loc,
454
+ direction: directions[loc],
455
+ title: baseMeta.title,
456
+ description: baseMeta.description,
457
+ origin,
458
+ realizations: localeArt.realizations,
459
+ locales,
460
+ defaultLocale,
461
+ });
462
+ out.push({
463
+ path: hit.path,
464
+ htmlPath: htmlPathForRoute(hit.path),
465
+ localeId: loc,
466
+ meta: {
467
+ title: baseMeta.title,
468
+ description: baseMeta.description,
469
+ canonical: built.canonical || absoluteUrl(origin, hit.path),
470
+ robots: baseMeta.robots,
471
+ lang: loc,
472
+ dir: directions[loc] || 'ltr',
473
+ alternates: built.alternates || [],
474
+ },
475
+ });
476
+ }
477
+ return out.length
478
+ ? out
479
+ : [
480
+ {
481
+ path: pattern,
482
+ htmlPath: htmlPathForRoute(pattern),
483
+ localeId: null,
484
+ meta: baseMeta,
485
+ },
486
+ ];
380
487
  }
381
488
  function guessTitle(pathname) {
382
489
  if (pathname === '/')
@@ -405,7 +512,7 @@ function readCssEntry(distDir) {
405
512
  * chunkId: string,
406
513
  * layoutChain: string[],
407
514
  * props: Record<string, unknown>,
408
- * meta: { title: string, description: string, canonical: string, robots: string, lang: string },
515
+ * meta: { title: string, description: string, canonical: string, robots: string, lang: string, dir?: string, alternates?: Array<{ hreflang: string, href: string }> },
409
516
  * cssEntry: string | null,
410
517
  * isErrorDocument?: boolean,
411
518
  * }} input
@@ -414,10 +521,17 @@ function wrapDocument(input) {
414
521
  const propsJson = JSON.stringify(input.props ?? {});
415
522
  const layoutAttr = input.layoutChain.length ? ` data-vmz-layout="${escapeAttr(input.layoutChain.join(','))}"` : '';
416
523
  const pageAttr = input.chunkId ? ` data-vmz-page="${escapeAttr(input.chunkId)}"` : '';
524
+ const localeId = input.meta.lang || 'en';
525
+ const dir = input.meta.dir || 'ltr';
526
+ const localeAttr = ` data-vmz-locale="${escapeAttr(localeId)}" data-vmz-dir="${escapeAttr(dir)}"`;
417
527
  const cssLink = input.cssEntry ? ` <link rel="stylesheet" href="/${String(input.cssEntry).replace(/^\/+/, '')}" />\n` : '';
418
528
  const entry = input.isErrorDocument ? '' : ` <script type="module" src="/entry-client.js"></script>\n`;
529
+ const hreflang = (input.meta.alternates || [])
530
+ .map((a) => ` <link rel="alternate" hreflang="${escapeAttr(a.hreflang)}" href="${escapeAttr(a.href)}" />`)
531
+ .join('\n');
532
+ const hreflangBlock = hreflang ? `${hreflang}\n` : '';
419
533
  return `<!DOCTYPE html>
420
- <html lang="${escapeAttr(input.meta.lang)}">
534
+ <html lang="${escapeAttr(localeId)}" data-locale="${escapeAttr(localeId)}" dir="${escapeAttr(dir)}">
421
535
  <head>
422
536
  <meta charset="utf-8" />
423
537
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -425,12 +539,12 @@ function wrapDocument(input) {
425
539
  <meta name="description" content="${escapeAttr(input.meta.description)}" />
426
540
  <meta name="robots" content="${escapeAttr(input.meta.robots)}" />
427
541
  <link rel="canonical" href="${escapeAttr(input.meta.canonical)}" />
428
- <meta property="og:title" content="${escapeAttr(input.meta.title)}" />
542
+ ${hreflangBlock} <meta property="og:title" content="${escapeAttr(input.meta.title)}" />
429
543
  <meta property="og:description" content="${escapeAttr(input.meta.description)}" />
430
544
  <meta property="og:url" content="${escapeAttr(input.meta.canonical)}" />
431
545
  ${cssLink}</head>
432
546
  <body>
433
- <div id="app"${pageAttr}${layoutAttr} data-vmz-props="${escapeAttr(propsJson)}">${input.bodyHtml}</div>
547
+ <div id="app"${pageAttr}${layoutAttr}${localeAttr} data-vmz-props="${escapeAttr(propsJson)}">${input.bodyHtml}</div>
434
548
  ${entry}</body>
435
549
  </html>
436
550
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/vmz",
3
- "version": "0.0.4",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "description": "VMZ Node toolchain — N-API workspace session + CLI (publish name @vmz/vmz)",
6
6
  "license": "MIT",
@@ -48,15 +48,15 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@vmz/core": "0.0.4",
52
- "@vmz/plugin": "0.0.4",
53
- "@vmz/protocol": "0.0.4",
51
+ "@vmz/core": "0.1.0",
52
+ "@vmz/plugin": "0.1.0",
53
+ "@vmz/protocol": "0.1.0",
54
54
  "jiti": "^2.6.1",
55
55
  "json5": "^2.2.3"
56
56
  },
57
57
  "peerDependencies": {
58
- "@vmz/plugin-markdown-it": "0.0.4",
59
- "@vmz/test": "0.0.4",
58
+ "@vmz/plugin-markdown-it": "0.1.0",
59
+ "@vmz/test": "0.1.0",
60
60
  "typescript": "^5.8.3"
61
61
  },
62
62
  "peerDependenciesMeta": {
@@ -90,12 +90,12 @@
90
90
  "cli"
91
91
  ],
92
92
  "optionalDependencies": {
93
- "@vmz/vmz-win32-x64": "0.0.4",
94
- "@vmz/vmz-win32-arm64": "0.0.4",
95
- "@vmz/vmz-darwin-x64": "0.0.4",
96
- "@vmz/vmz-darwin-arm64": "0.0.4",
97
- "@vmz/vmz-linux-x64": "0.0.4",
98
- "@vmz/vmz-linux-arm64": "0.0.4"
93
+ "@vmz/vmz-win32-x64": "0.1.0",
94
+ "@vmz/vmz-win32-arm64": "0.1.0",
95
+ "@vmz/vmz-darwin-x64": "0.1.0",
96
+ "@vmz/vmz-darwin-arm64": "0.1.0",
97
+ "@vmz/vmz-linux-x64": "0.1.0",
98
+ "@vmz/vmz-linux-arm64": "0.1.0"
99
99
  },
100
100
  "publishConfig": {
101
101
  "access": "public"