astro-intlayer 9.3.1 → 9.3.3

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.
@@ -0,0 +1,102 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let node_fs_promises = require("node:fs/promises");
3
+ let node_path = require("node:path");
4
+ let node_url = require("node:url");
5
+ let _intlayer_core_localization = require("@intlayer/core/localization");
6
+
7
+ //#region src/emitRewrittenPages.ts
8
+ /**
9
+ * Recursively lists every `.html` file contained in a directory.
10
+ */
11
+ const listHtmlFiles = async (directory) => {
12
+ const entries = await (0, node_fs_promises.readdir)(directory, { withFileTypes: true });
13
+ return (await Promise.all(entries.map(async (entry) => {
14
+ const entryPath = (0, node_path.join)(directory, entry.name);
15
+ if (entry.isDirectory()) return listHtmlFiles(entryPath);
16
+ return entry.isFile() && entry.name.endsWith(".html") ? [entryPath] : [];
17
+ }))).flat();
18
+ };
19
+ /**
20
+ * Converts an emitted HTML file path into the URL path it is served at.
21
+ *
22
+ * - `about/index.html` → `/about`
23
+ * - `about.html` → `/about`
24
+ * - `index.html` → `/`
25
+ */
26
+ const toBuiltPage = (outputDirectory, filePath) => {
27
+ const relativePath = (0, node_path.relative)(outputDirectory, filePath).split(node_path.sep).join("/");
28
+ const isDirectoryIndex = relativePath.endsWith("index.html");
29
+ return {
30
+ filePath,
31
+ urlPath: `/${isDirectoryIndex ? relativePath.slice(0, -10).replace(/\/$/, "") : relativePath.slice(0, -5)}`.replace(/\/{2,}/g, "/"),
32
+ isDirectoryIndex
33
+ };
34
+ };
35
+ /**
36
+ * Splits a URL path into its locale prefix (when present) and the remainder.
37
+ */
38
+ const splitLocalePrefix = (urlPath, locales) => {
39
+ const firstSegment = urlPath.split("/")[1];
40
+ if (firstSegment && locales.includes(firstSegment)) return {
41
+ localePrefix: `/${firstSegment}`,
42
+ pathWithoutLocale: urlPath.slice(firstSegment.length + 1) || "/"
43
+ };
44
+ return {
45
+ localePrefix: "",
46
+ pathWithoutLocale: urlPath || "/"
47
+ };
48
+ };
49
+ /**
50
+ * Maps a URL path back onto the on-disk layout Astro used for the source page,
51
+ * so the emitted twin keeps the same `directory` / `file` build format.
52
+ */
53
+ const toFilePath = (outputDirectory, urlPath, isDirectoryIndex) => {
54
+ const trimmedPath = urlPath.replace(/^\//, "");
55
+ return (0, node_path.join)(outputDirectory, isDirectoryIndex ? (0, node_path.join)(trimmedPath, "index.html") : `${trimmedPath}.html`);
56
+ };
57
+ /**
58
+ * Emits a copy of every prerendered page at its rewritten ("pretty") URL.
59
+ *
60
+ * Astro renders pages from their canonical file-system route (`/about`,
61
+ * `/en/about`), so a static build contains no file for the localized paths
62
+ * declared in `routing.rewrite` (`/nosotros`). The dev and SSR proxies resolve
63
+ * those paths at request time, but a static host has nothing to serve and
64
+ * answers 404 — even though `getLocalizedUrl` (links, hreflang, sitemap)
65
+ * already points at them.
66
+ *
67
+ * This mirrors each canonical page onto its localized path at the end of the
68
+ * build. The canonical path is kept reachable, matching the proxy behaviour.
69
+ *
70
+ * @param configuration - The resolved Intlayer configuration.
71
+ * @param outputDirectoryUrl - The build output directory, as given by `astro:build:done`.
72
+ * @returns The list of `[from, to]` URL paths that were emitted.
73
+ */
74
+ const emitRewrittenPages = async (configuration, outputDirectoryUrl) => {
75
+ const { routing, internationalization } = configuration;
76
+ const rewriteRules = (0, _intlayer_core_localization.getRewriteRules)(routing.rewrite, "url");
77
+ const isPrefixMode = routing.mode === "prefix-all" || routing.mode === "prefix-no-default";
78
+ if (!rewriteRules || !isPrefixMode) return [];
79
+ const locales = internationalization.locales;
80
+ const defaultLocale = internationalization.defaultLocale;
81
+ const outputDirectory = (0, node_url.fileURLToPath)(outputDirectoryUrl);
82
+ const htmlFiles = await listHtmlFiles(outputDirectory);
83
+ const emittedPages = [];
84
+ for (const htmlFile of htmlFiles) {
85
+ const { filePath, urlPath, isDirectoryIndex } = toBuiltPage(outputDirectory, htmlFile);
86
+ const { localePrefix, pathWithoutLocale } = splitLocalePrefix(urlPath, locales);
87
+ const locale = localePrefix.slice(1) || defaultLocale;
88
+ const canonicalPath = (0, _intlayer_core_localization.getCanonicalPath)(pathWithoutLocale, locale, rewriteRules);
89
+ const { path: localizedPath, isRewritten } = (0, _intlayer_core_localization.resolveLocalizedPath)(canonicalPath, locale, rewriteRules);
90
+ if (!isRewritten || localizedPath === pathWithoutLocale) continue;
91
+ const targetUrlPath = `${localePrefix}${localizedPath}`.replace(/\/{2,}/g, "/");
92
+ const targetFilePath = toFilePath(outputDirectory, targetUrlPath, isDirectoryIndex);
93
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(targetFilePath), { recursive: true });
94
+ await (0, node_fs_promises.copyFile)(filePath, targetFilePath);
95
+ emittedPages.push([urlPath, targetUrlPath]);
96
+ }
97
+ return emittedPages;
98
+ };
99
+
100
+ //#endregion
101
+ exports.emitRewrittenPages = emitRewrittenPages;
102
+ //# sourceMappingURL=emitRewrittenPages.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emitRewrittenPages.cjs","names":["readdir","join","relative","sep","getRewriteRules","fileURLToPath","getCanonicalPath","resolveLocalizedPath","mkdir","dirname","copyFile"],"sources":["../../src/emitRewrittenPages.ts"],"sourcesContent":["import { copyFile, mkdir, readdir } from 'node:fs/promises';\nimport { dirname, join, relative, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n getCanonicalPath,\n getRewriteRules,\n resolveLocalizedPath,\n} from '@intlayer/core/localization';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n\n/**\n * Description of a built HTML page, expressed both as the URL path it is served\n * at and as the on-disk layout Astro used to emit it.\n */\ntype BuiltPage = {\n /** Absolute path of the emitted HTML file. */\n filePath: string;\n /** URL path the file is served at, without trailing slash (e.g. `/en/about`). */\n urlPath: string;\n /** Whether the file is a directory index (`about/index.html`) or flat (`about.html`). */\n isDirectoryIndex: boolean;\n};\n\n/**\n * Recursively lists every `.html` file contained in a directory.\n */\nconst listHtmlFiles = async (directory: string): Promise<string[]> => {\n const entries = await readdir(directory, { withFileTypes: true });\n\n const nestedFiles = await Promise.all(\n entries.map(async (entry) => {\n const entryPath = join(directory, entry.name);\n\n if (entry.isDirectory()) return listHtmlFiles(entryPath);\n\n return entry.isFile() && entry.name.endsWith('.html') ? [entryPath] : [];\n })\n );\n\n return nestedFiles.flat();\n};\n\n/**\n * Converts an emitted HTML file path into the URL path it is served at.\n *\n * - `about/index.html` → `/about`\n * - `about.html` → `/about`\n * - `index.html` → `/`\n */\nconst toBuiltPage = (outputDirectory: string, filePath: string): BuiltPage => {\n const relativePath = relative(outputDirectory, filePath).split(sep).join('/');\n\n const isDirectoryIndex = relativePath.endsWith('index.html');\n\n const pathWithoutExtension = isDirectoryIndex\n ? relativePath.slice(0, -'index.html'.length).replace(/\\/$/, '')\n : relativePath.slice(0, -'.html'.length);\n\n return {\n filePath,\n urlPath: `/${pathWithoutExtension}`.replace(/\\/{2,}/g, '/'),\n isDirectoryIndex,\n };\n};\n\n/**\n * Splits a URL path into its locale prefix (when present) and the remainder.\n */\nconst splitLocalePrefix = (\n urlPath: string,\n locales: Locale[]\n): { localePrefix: string; pathWithoutLocale: string } => {\n const firstSegment = urlPath.split('/')[1];\n\n if (firstSegment && locales.includes(firstSegment as Locale)) {\n return {\n localePrefix: `/${firstSegment}`,\n pathWithoutLocale: urlPath.slice(firstSegment.length + 1) || '/',\n };\n }\n\n return { localePrefix: '', pathWithoutLocale: urlPath || '/' };\n};\n\n/**\n * Maps a URL path back onto the on-disk layout Astro used for the source page,\n * so the emitted twin keeps the same `directory` / `file` build format.\n */\nconst toFilePath = (\n outputDirectory: string,\n urlPath: string,\n isDirectoryIndex: boolean\n): string => {\n const trimmedPath = urlPath.replace(/^\\//, '');\n\n return join(\n outputDirectory,\n isDirectoryIndex ? join(trimmedPath, 'index.html') : `${trimmedPath}.html`\n );\n};\n\n/**\n * Emits a copy of every prerendered page at its rewritten (\"pretty\") URL.\n *\n * Astro renders pages from their canonical file-system route (`/about`,\n * `/en/about`), so a static build contains no file for the localized paths\n * declared in `routing.rewrite` (`/nosotros`). The dev and SSR proxies resolve\n * those paths at request time, but a static host has nothing to serve and\n * answers 404 — even though `getLocalizedUrl` (links, hreflang, sitemap)\n * already points at them.\n *\n * This mirrors each canonical page onto its localized path at the end of the\n * build. The canonical path is kept reachable, matching the proxy behaviour.\n *\n * @param configuration - The resolved Intlayer configuration.\n * @param outputDirectoryUrl - The build output directory, as given by `astro:build:done`.\n * @returns The list of `[from, to]` URL paths that were emitted.\n */\nexport const emitRewrittenPages = async (\n configuration: IntlayerConfig,\n outputDirectoryUrl: URL\n): Promise<[from: string, to: string][]> => {\n const { routing, internationalization } = configuration;\n\n const rewriteRules = getRewriteRules(routing.rewrite, 'url');\n\n // Without prefixes a single file serves every locale, so a per-locale\n // rewrite cannot be resolved from the file path alone.\n const isPrefixMode =\n routing.mode === 'prefix-all' || routing.mode === 'prefix-no-default';\n\n if (!rewriteRules || !isPrefixMode) return [];\n\n const locales = internationalization.locales as Locale[];\n const defaultLocale = internationalization.defaultLocale as Locale;\n\n const outputDirectory = fileURLToPath(outputDirectoryUrl);\n const htmlFiles = await listHtmlFiles(outputDirectory);\n\n const emittedPages: [from: string, to: string][] = [];\n\n for (const htmlFile of htmlFiles) {\n const { filePath, urlPath, isDirectoryIndex } = toBuiltPage(\n outputDirectory,\n htmlFile\n );\n\n const { localePrefix, pathWithoutLocale } = splitLocalePrefix(\n urlPath,\n locales\n );\n\n // An unprefixed path is only reachable when the default locale is not\n // prefixed, in which case it belongs to the default locale.\n const locale = (localePrefix.slice(1) || defaultLocale) as Locale;\n\n const canonicalPath = getCanonicalPath(\n pathWithoutLocale,\n locale,\n rewriteRules\n );\n\n const { path: localizedPath, isRewritten } = resolveLocalizedPath(\n canonicalPath,\n locale,\n rewriteRules\n );\n\n // Either no rule matches, or the page is already emitted at its pretty URL.\n if (!isRewritten || localizedPath === pathWithoutLocale) continue;\n\n const targetUrlPath = `${localePrefix}${localizedPath}`.replace(\n /\\/{2,}/g,\n '/'\n );\n const targetFilePath = toFilePath(\n outputDirectory,\n targetUrlPath,\n isDirectoryIndex\n );\n\n await mkdir(dirname(targetFilePath), { recursive: true });\n await copyFile(filePath, targetFilePath);\n\n emittedPages.push([urlPath, targetUrlPath]);\n }\n\n return emittedPages;\n};\n"],"mappings":";;;;;;;;;;AA2BA,MAAM,gBAAgB,OAAO,cAAyC;CACpE,MAAM,UAAU,UAAMA,0BAAQ,WAAW,EAAE,eAAe,KAAK,CAAC;CAYhE,QAAO,MAVmB,QAAQ,IAChC,QAAQ,IAAI,OAAO,UAAU;EAC3B,MAAM,gBAAYC,gBAAK,WAAW,MAAM,IAAI;EAE5C,IAAI,MAAM,YAAY,GAAG,OAAO,cAAc,SAAS;EAEvD,OAAO,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS,OAAO,IAAI,CAAC,SAAS,IAAI,CAAC;CACzE,CAAC,CACH,EAEkB,CAAC,KAAK;AAC1B;;;;;;;;AASA,MAAM,eAAe,iBAAyB,aAAgC;CAC5E,MAAM,mBAAeC,oBAAS,iBAAiB,QAAQ,CAAC,CAAC,MAAMC,aAAG,CAAC,CAAC,KAAK,GAAG;CAE5E,MAAM,mBAAmB,aAAa,SAAS,YAAY;CAM3D,OAAO;EACL;EACA,SAAS,IANkB,mBACzB,aAAa,MAAM,GAAG,GAAoB,CAAC,CAAC,QAAQ,OAAO,EAAE,IAC7D,aAAa,MAAM,GAAG,EAAe,IAIH,QAAQ,WAAW,GAAG;EAC1D;CACF;AACF;;;;AAKA,MAAM,qBACJ,SACA,YACwD;CACxD,MAAM,eAAe,QAAQ,MAAM,GAAG,CAAC,CAAC;CAExC,IAAI,gBAAgB,QAAQ,SAAS,YAAsB,GACzD,OAAO;EACL,cAAc,IAAI;EAClB,mBAAmB,QAAQ,MAAM,aAAa,SAAS,CAAC,KAAK;CAC/D;CAGF,OAAO;EAAE,cAAc;EAAI,mBAAmB,WAAW;CAAI;AAC/D;;;;;AAMA,MAAM,cACJ,iBACA,SACA,qBACW;CACX,MAAM,cAAc,QAAQ,QAAQ,OAAO,EAAE;CAE7C,WAAOF,gBACL,iBACA,uBAAmBA,gBAAK,aAAa,YAAY,IAAI,GAAG,YAAY,MACtE;AACF;;;;;;;;;;;;;;;;;;AAmBA,MAAa,qBAAqB,OAChC,eACA,uBAC0C;CAC1C,MAAM,EAAE,SAAS,yBAAyB;CAE1C,MAAM,mBAAeG,6CAAgB,QAAQ,SAAS,KAAK;CAI3D,MAAM,eACJ,QAAQ,SAAS,gBAAgB,QAAQ,SAAS;CAEpD,IAAI,CAAC,gBAAgB,CAAC,cAAc,OAAO,CAAC;CAE5C,MAAM,UAAU,qBAAqB;CACrC,MAAM,gBAAgB,qBAAqB;CAE3C,MAAM,sBAAkBC,wBAAc,kBAAkB;CACxD,MAAM,YAAY,MAAM,cAAc,eAAe;CAErD,MAAM,eAA6C,CAAC;CAEpD,KAAK,MAAM,YAAY,WAAW;EAChC,MAAM,EAAE,UAAU,SAAS,qBAAqB,YAC9C,iBACA,QACF;EAEA,MAAM,EAAE,cAAc,sBAAsB,kBAC1C,SACA,OACF;EAIA,MAAM,SAAU,aAAa,MAAM,CAAC,KAAK;EAEzC,MAAM,oBAAgBC,8CACpB,mBACA,QACA,YACF;EAEA,MAAM,EAAE,MAAM,eAAe,oBAAgBC,kDAC3C,eACA,QACA,YACF;EAGA,IAAI,CAAC,eAAe,kBAAkB,mBAAmB;EAEzD,MAAM,gBAAgB,GAAG,eAAe,gBAAgB,QACtD,WACA,GACF;EACA,MAAM,iBAAiB,WACrB,iBACA,eACA,gBACF;EAEA,UAAMC,4BAAMC,mBAAQ,cAAc,GAAG,EAAE,WAAW,KAAK,CAAC;EACxD,UAAMC,2BAAS,UAAU,cAAc;EAEvC,aAAa,KAAK,CAAC,SAAS,aAAa,CAAC;CAC5C;CAEA,OAAO;AACT"}
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_emitRewrittenPages = require('./emitRewrittenPages.cjs');
2
3
  let node_path = require("node:path");
3
4
  let _intlayer_config_node = require("@intlayer/config/node");
4
5
  let _intlayer_config_utils = require("@intlayer/config/utils");
@@ -15,6 +16,7 @@ let vite_intlayer = require("vite-intlayer");
15
16
  * 2. Injecting Vite plugins for aliases, locale-based routing (middleware), and build optimizations (prune).
16
17
  * 3. Configuring Vite aliases for dictionary access.
17
18
  * 4. Starting a file watcher for dictionary changes during development.
19
+ * 5. Emitting the prerendered pages at their rewritten (localized) URLs.
18
20
  *
19
21
  * @returns An Astro integration object.
20
22
  *
@@ -46,6 +48,11 @@ const intlayer = () => ({
46
48
  "astro:server:setup": async () => {
47
49
  const configuration = (0, _intlayer_config_node.getConfiguration)();
48
50
  if (configuration.content.watch) await (0, _intlayer_engine_watcher.watch)({ configuration });
51
+ },
52
+ "astro:build:done": async ({ dir, logger }) => {
53
+ const configuration = (0, _intlayer_config_node.getConfiguration)();
54
+ const emittedPages = await require_emitRewrittenPages.emitRewrittenPages(configuration, dir);
55
+ if (emittedPages.length > 0) logger.info(`Emitted ${emittedPages.length} rewritten page(s): ${emittedPages.map(([from, to]) => `${from} \u2192 ${to}`).join(", ")}`);
49
56
  }
50
57
  }
51
58
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["getConfiguration","prepareIntlayer","viteIntlayerPlugin","viteIntlayerProxyPlugin","getAlias","resolve","watch"],"sources":["../../src/index.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getAlias } from '@intlayer/config/utils';\nimport { prepareIntlayer } from '@intlayer/engine/build';\nimport { watch } from '@intlayer/engine/watcher';\nimport type { AstroIntegration } from 'astro';\nimport type { PluginOption } from 'vite';\nimport {\n intlayer as viteIntlayerPlugin,\n intlayerProxy as viteIntlayerProxyPlugin,\n} from 'vite-intlayer';\n\n/**\n * Astro integration for Intlayer.\n *\n * It handles:\n * 1. Preparing Intlayer resources (dictionaries) at config setup.\n * 2. Injecting Vite plugins for aliases, locale-based routing (middleware), and build optimizations (prune).\n * 3. Configuring Vite aliases for dictionary access.\n * 4. Starting a file watcher for dictionary changes during development.\n *\n * @returns An Astro integration object.\n *\n * @example\n * ```ts\n * // astro.config.mjs\n * import { defineConfig } from 'astro/config';\n * import { intlayer } from 'astro-intlayer';\n *\n * export default defineConfig({\n * integrations: [intlayer()],\n * });\n * ```\n */\nexport const intlayer = (): AstroIntegration =>\n ({\n name: 'astro-intlayer',\n hooks: {\n 'astro:config:setup': async ({ updateConfig }) => {\n const configuration = getConfiguration();\n\n // Prepare once per process start to ensure generated entries exist\n await prepareIntlayer(configuration);\n\n updateConfig({\n vite: {\n plugins: [\n // Aliases + watcher + buildStart prep\n // (also handles optimize/prune/minify internally)\n viteIntlayerPlugin(),\n // Dev-time middleware for locale routing\n viteIntlayerProxyPlugin(),\n ] as PluginOption[],\n resolve: {\n alias: {\n ...getAlias({\n configuration,\n formatter: (value) => resolve(value),\n }),\n },\n },\n },\n });\n },\n\n 'astro:server:setup': async () => {\n const configuration = getConfiguration();\n\n if (configuration.content.watch) {\n await watch({ configuration });\n }\n },\n },\n }) satisfies AstroIntegration;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,kBACV;CACC,MAAM;CACN,OAAO;EACL,sBAAsB,OAAO,EAAE,mBAAmB;GAChD,MAAM,oBAAgBA,wCAAiB;GAGvC,UAAMC,wCAAgB,aAAa;GAEnC,aAAa,EACX,MAAM;IACJ,SAAS,KAGPC,wBAAmB,OAEnBC,6BAAwB,CAC1B;IACA,SAAS,EACP,OAAO,EACL,OAAGC,iCAAS;KACV;KACA,YAAY,cAAUC,mBAAQ,KAAK;IACrC,CAAC,EACH,EACF;GACF,EACF,CAAC;EACH;EAEA,sBAAsB,YAAY;GAChC,MAAM,oBAAgBL,wCAAiB;GAEvC,IAAI,cAAc,QAAQ,OACxB,UAAMM,gCAAM,EAAE,cAAc,CAAC;EAEjC;CACF;AACF"}
1
+ {"version":3,"file":"index.cjs","names":["getConfiguration","prepareIntlayer","viteIntlayerPlugin","viteIntlayerProxyPlugin","getAlias","resolve","watch","emitRewrittenPages"],"sources":["../../src/index.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getAlias } from '@intlayer/config/utils';\nimport { prepareIntlayer } from '@intlayer/engine/build';\nimport { watch } from '@intlayer/engine/watcher';\nimport type { AstroIntegration } from 'astro';\nimport type { PluginOption } from 'vite';\nimport {\n intlayer as viteIntlayerPlugin,\n intlayerProxy as viteIntlayerProxyPlugin,\n} from 'vite-intlayer';\nimport { emitRewrittenPages } from './emitRewrittenPages';\n\n/**\n * Astro integration for Intlayer.\n *\n * It handles:\n * 1. Preparing Intlayer resources (dictionaries) at config setup.\n * 2. Injecting Vite plugins for aliases, locale-based routing (middleware), and build optimizations (prune).\n * 3. Configuring Vite aliases for dictionary access.\n * 4. Starting a file watcher for dictionary changes during development.\n * 5. Emitting the prerendered pages at their rewritten (localized) URLs.\n *\n * @returns An Astro integration object.\n *\n * @example\n * ```ts\n * // astro.config.mjs\n * import { defineConfig } from 'astro/config';\n * import { intlayer } from 'astro-intlayer';\n *\n * export default defineConfig({\n * integrations: [intlayer()],\n * });\n * ```\n */\nexport const intlayer = (): AstroIntegration =>\n ({\n name: 'astro-intlayer',\n hooks: {\n 'astro:config:setup': async ({ updateConfig }) => {\n const configuration = getConfiguration();\n\n // Prepare once per process start to ensure generated entries exist\n await prepareIntlayer(configuration);\n\n updateConfig({\n vite: {\n plugins: [\n // Aliases + watcher + buildStart prep\n // (also handles optimize/prune/minify internally)\n viteIntlayerPlugin(),\n // Dev-time middleware for locale routing\n viteIntlayerProxyPlugin(),\n ] as PluginOption[],\n resolve: {\n alias: {\n ...getAlias({\n configuration,\n formatter: (value) => resolve(value),\n }),\n },\n },\n },\n });\n },\n\n 'astro:server:setup': async () => {\n const configuration = getConfiguration();\n\n if (configuration.content.watch) {\n await watch({ configuration });\n }\n },\n\n // Astro renders each page from its canonical file-system route, so a\n // static build has no file for the localized paths declared in\n // `routing.rewrite`. Mirror them here, otherwise the URLs produced by\n // `getLocalizedUrl` (links, hreflang, sitemap) 404 once deployed.\n 'astro:build:done': async ({ dir, logger }) => {\n const configuration = getConfiguration();\n\n const emittedPages = await emitRewrittenPages(configuration, dir);\n\n if (emittedPages.length > 0) {\n logger.info(\n `Emitted ${emittedPages.length} rewritten page(s): ${emittedPages\n .map(([from, to]) => `${from} \\u2192 ${to}`)\n .join(', ')}`\n );\n }\n },\n },\n }) satisfies AstroIntegration;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,MAAa,kBACV;CACC,MAAM;CACN,OAAO;EACL,sBAAsB,OAAO,EAAE,mBAAmB;GAChD,MAAM,oBAAgBA,wCAAiB;GAGvC,UAAMC,wCAAgB,aAAa;GAEnC,aAAa,EACX,MAAM;IACJ,SAAS,KAGPC,wBAAmB,OAEnBC,6BAAwB,CAC1B;IACA,SAAS,EACP,OAAO,EACL,OAAGC,iCAAS;KACV;KACA,YAAY,cAAUC,mBAAQ,KAAK;IACrC,CAAC,EACH,EACF;GACF,EACF,CAAC;EACH;EAEA,sBAAsB,YAAY;GAChC,MAAM,oBAAgBL,wCAAiB;GAEvC,IAAI,cAAc,QAAQ,OACxB,UAAMM,gCAAM,EAAE,cAAc,CAAC;EAEjC;EAMA,oBAAoB,OAAO,EAAE,KAAK,aAAa;GAC7C,MAAM,oBAAgBN,wCAAiB;GAEvC,MAAM,eAAe,MAAMO,8CAAmB,eAAe,GAAG;GAEhE,IAAI,aAAa,SAAS,GACxB,OAAO,KACL,WAAW,aAAa,OAAO,sBAAsB,aAClD,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,UAAU,IAAI,CAAC,CAC3C,KAAK,IAAI,GACd;EAEJ;CACF;AACF"}
@@ -0,0 +1,101 @@
1
+ import { copyFile, mkdir, readdir } from "node:fs/promises";
2
+ import { dirname, join, relative, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { getCanonicalPath, getRewriteRules, resolveLocalizedPath } from "@intlayer/core/localization";
5
+
6
+ //#region src/emitRewrittenPages.ts
7
+ /**
8
+ * Recursively lists every `.html` file contained in a directory.
9
+ */
10
+ const listHtmlFiles = async (directory) => {
11
+ const entries = await readdir(directory, { withFileTypes: true });
12
+ return (await Promise.all(entries.map(async (entry) => {
13
+ const entryPath = join(directory, entry.name);
14
+ if (entry.isDirectory()) return listHtmlFiles(entryPath);
15
+ return entry.isFile() && entry.name.endsWith(".html") ? [entryPath] : [];
16
+ }))).flat();
17
+ };
18
+ /**
19
+ * Converts an emitted HTML file path into the URL path it is served at.
20
+ *
21
+ * - `about/index.html` → `/about`
22
+ * - `about.html` → `/about`
23
+ * - `index.html` → `/`
24
+ */
25
+ const toBuiltPage = (outputDirectory, filePath) => {
26
+ const relativePath = relative(outputDirectory, filePath).split(sep).join("/");
27
+ const isDirectoryIndex = relativePath.endsWith("index.html");
28
+ return {
29
+ filePath,
30
+ urlPath: `/${isDirectoryIndex ? relativePath.slice(0, -10).replace(/\/$/, "") : relativePath.slice(0, -5)}`.replace(/\/{2,}/g, "/"),
31
+ isDirectoryIndex
32
+ };
33
+ };
34
+ /**
35
+ * Splits a URL path into its locale prefix (when present) and the remainder.
36
+ */
37
+ const splitLocalePrefix = (urlPath, locales) => {
38
+ const firstSegment = urlPath.split("/")[1];
39
+ if (firstSegment && locales.includes(firstSegment)) return {
40
+ localePrefix: `/${firstSegment}`,
41
+ pathWithoutLocale: urlPath.slice(firstSegment.length + 1) || "/"
42
+ };
43
+ return {
44
+ localePrefix: "",
45
+ pathWithoutLocale: urlPath || "/"
46
+ };
47
+ };
48
+ /**
49
+ * Maps a URL path back onto the on-disk layout Astro used for the source page,
50
+ * so the emitted twin keeps the same `directory` / `file` build format.
51
+ */
52
+ const toFilePath = (outputDirectory, urlPath, isDirectoryIndex) => {
53
+ const trimmedPath = urlPath.replace(/^\//, "");
54
+ return join(outputDirectory, isDirectoryIndex ? join(trimmedPath, "index.html") : `${trimmedPath}.html`);
55
+ };
56
+ /**
57
+ * Emits a copy of every prerendered page at its rewritten ("pretty") URL.
58
+ *
59
+ * Astro renders pages from their canonical file-system route (`/about`,
60
+ * `/en/about`), so a static build contains no file for the localized paths
61
+ * declared in `routing.rewrite` (`/nosotros`). The dev and SSR proxies resolve
62
+ * those paths at request time, but a static host has nothing to serve and
63
+ * answers 404 — even though `getLocalizedUrl` (links, hreflang, sitemap)
64
+ * already points at them.
65
+ *
66
+ * This mirrors each canonical page onto its localized path at the end of the
67
+ * build. The canonical path is kept reachable, matching the proxy behaviour.
68
+ *
69
+ * @param configuration - The resolved Intlayer configuration.
70
+ * @param outputDirectoryUrl - The build output directory, as given by `astro:build:done`.
71
+ * @returns The list of `[from, to]` URL paths that were emitted.
72
+ */
73
+ const emitRewrittenPages = async (configuration, outputDirectoryUrl) => {
74
+ const { routing, internationalization } = configuration;
75
+ const rewriteRules = getRewriteRules(routing.rewrite, "url");
76
+ const isPrefixMode = routing.mode === "prefix-all" || routing.mode === "prefix-no-default";
77
+ if (!rewriteRules || !isPrefixMode) return [];
78
+ const locales = internationalization.locales;
79
+ const defaultLocale = internationalization.defaultLocale;
80
+ const outputDirectory = fileURLToPath(outputDirectoryUrl);
81
+ const htmlFiles = await listHtmlFiles(outputDirectory);
82
+ const emittedPages = [];
83
+ for (const htmlFile of htmlFiles) {
84
+ const { filePath, urlPath, isDirectoryIndex } = toBuiltPage(outputDirectory, htmlFile);
85
+ const { localePrefix, pathWithoutLocale } = splitLocalePrefix(urlPath, locales);
86
+ const locale = localePrefix.slice(1) || defaultLocale;
87
+ const canonicalPath = getCanonicalPath(pathWithoutLocale, locale, rewriteRules);
88
+ const { path: localizedPath, isRewritten } = resolveLocalizedPath(canonicalPath, locale, rewriteRules);
89
+ if (!isRewritten || localizedPath === pathWithoutLocale) continue;
90
+ const targetUrlPath = `${localePrefix}${localizedPath}`.replace(/\/{2,}/g, "/");
91
+ const targetFilePath = toFilePath(outputDirectory, targetUrlPath, isDirectoryIndex);
92
+ await mkdir(dirname(targetFilePath), { recursive: true });
93
+ await copyFile(filePath, targetFilePath);
94
+ emittedPages.push([urlPath, targetUrlPath]);
95
+ }
96
+ return emittedPages;
97
+ };
98
+
99
+ //#endregion
100
+ export { emitRewrittenPages };
101
+ //# sourceMappingURL=emitRewrittenPages.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emitRewrittenPages.mjs","names":[],"sources":["../../src/emitRewrittenPages.ts"],"sourcesContent":["import { copyFile, mkdir, readdir } from 'node:fs/promises';\nimport { dirname, join, relative, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n getCanonicalPath,\n getRewriteRules,\n resolveLocalizedPath,\n} from '@intlayer/core/localization';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n\n/**\n * Description of a built HTML page, expressed both as the URL path it is served\n * at and as the on-disk layout Astro used to emit it.\n */\ntype BuiltPage = {\n /** Absolute path of the emitted HTML file. */\n filePath: string;\n /** URL path the file is served at, without trailing slash (e.g. `/en/about`). */\n urlPath: string;\n /** Whether the file is a directory index (`about/index.html`) or flat (`about.html`). */\n isDirectoryIndex: boolean;\n};\n\n/**\n * Recursively lists every `.html` file contained in a directory.\n */\nconst listHtmlFiles = async (directory: string): Promise<string[]> => {\n const entries = await readdir(directory, { withFileTypes: true });\n\n const nestedFiles = await Promise.all(\n entries.map(async (entry) => {\n const entryPath = join(directory, entry.name);\n\n if (entry.isDirectory()) return listHtmlFiles(entryPath);\n\n return entry.isFile() && entry.name.endsWith('.html') ? [entryPath] : [];\n })\n );\n\n return nestedFiles.flat();\n};\n\n/**\n * Converts an emitted HTML file path into the URL path it is served at.\n *\n * - `about/index.html` → `/about`\n * - `about.html` → `/about`\n * - `index.html` → `/`\n */\nconst toBuiltPage = (outputDirectory: string, filePath: string): BuiltPage => {\n const relativePath = relative(outputDirectory, filePath).split(sep).join('/');\n\n const isDirectoryIndex = relativePath.endsWith('index.html');\n\n const pathWithoutExtension = isDirectoryIndex\n ? relativePath.slice(0, -'index.html'.length).replace(/\\/$/, '')\n : relativePath.slice(0, -'.html'.length);\n\n return {\n filePath,\n urlPath: `/${pathWithoutExtension}`.replace(/\\/{2,}/g, '/'),\n isDirectoryIndex,\n };\n};\n\n/**\n * Splits a URL path into its locale prefix (when present) and the remainder.\n */\nconst splitLocalePrefix = (\n urlPath: string,\n locales: Locale[]\n): { localePrefix: string; pathWithoutLocale: string } => {\n const firstSegment = urlPath.split('/')[1];\n\n if (firstSegment && locales.includes(firstSegment as Locale)) {\n return {\n localePrefix: `/${firstSegment}`,\n pathWithoutLocale: urlPath.slice(firstSegment.length + 1) || '/',\n };\n }\n\n return { localePrefix: '', pathWithoutLocale: urlPath || '/' };\n};\n\n/**\n * Maps a URL path back onto the on-disk layout Astro used for the source page,\n * so the emitted twin keeps the same `directory` / `file` build format.\n */\nconst toFilePath = (\n outputDirectory: string,\n urlPath: string,\n isDirectoryIndex: boolean\n): string => {\n const trimmedPath = urlPath.replace(/^\\//, '');\n\n return join(\n outputDirectory,\n isDirectoryIndex ? join(trimmedPath, 'index.html') : `${trimmedPath}.html`\n );\n};\n\n/**\n * Emits a copy of every prerendered page at its rewritten (\"pretty\") URL.\n *\n * Astro renders pages from their canonical file-system route (`/about`,\n * `/en/about`), so a static build contains no file for the localized paths\n * declared in `routing.rewrite` (`/nosotros`). The dev and SSR proxies resolve\n * those paths at request time, but a static host has nothing to serve and\n * answers 404 — even though `getLocalizedUrl` (links, hreflang, sitemap)\n * already points at them.\n *\n * This mirrors each canonical page onto its localized path at the end of the\n * build. The canonical path is kept reachable, matching the proxy behaviour.\n *\n * @param configuration - The resolved Intlayer configuration.\n * @param outputDirectoryUrl - The build output directory, as given by `astro:build:done`.\n * @returns The list of `[from, to]` URL paths that were emitted.\n */\nexport const emitRewrittenPages = async (\n configuration: IntlayerConfig,\n outputDirectoryUrl: URL\n): Promise<[from: string, to: string][]> => {\n const { routing, internationalization } = configuration;\n\n const rewriteRules = getRewriteRules(routing.rewrite, 'url');\n\n // Without prefixes a single file serves every locale, so a per-locale\n // rewrite cannot be resolved from the file path alone.\n const isPrefixMode =\n routing.mode === 'prefix-all' || routing.mode === 'prefix-no-default';\n\n if (!rewriteRules || !isPrefixMode) return [];\n\n const locales = internationalization.locales as Locale[];\n const defaultLocale = internationalization.defaultLocale as Locale;\n\n const outputDirectory = fileURLToPath(outputDirectoryUrl);\n const htmlFiles = await listHtmlFiles(outputDirectory);\n\n const emittedPages: [from: string, to: string][] = [];\n\n for (const htmlFile of htmlFiles) {\n const { filePath, urlPath, isDirectoryIndex } = toBuiltPage(\n outputDirectory,\n htmlFile\n );\n\n const { localePrefix, pathWithoutLocale } = splitLocalePrefix(\n urlPath,\n locales\n );\n\n // An unprefixed path is only reachable when the default locale is not\n // prefixed, in which case it belongs to the default locale.\n const locale = (localePrefix.slice(1) || defaultLocale) as Locale;\n\n const canonicalPath = getCanonicalPath(\n pathWithoutLocale,\n locale,\n rewriteRules\n );\n\n const { path: localizedPath, isRewritten } = resolveLocalizedPath(\n canonicalPath,\n locale,\n rewriteRules\n );\n\n // Either no rule matches, or the page is already emitted at its pretty URL.\n if (!isRewritten || localizedPath === pathWithoutLocale) continue;\n\n const targetUrlPath = `${localePrefix}${localizedPath}`.replace(\n /\\/{2,}/g,\n '/'\n );\n const targetFilePath = toFilePath(\n outputDirectory,\n targetUrlPath,\n isDirectoryIndex\n );\n\n await mkdir(dirname(targetFilePath), { recursive: true });\n await copyFile(filePath, targetFilePath);\n\n emittedPages.push([urlPath, targetUrlPath]);\n }\n\n return emittedPages;\n};\n"],"mappings":";;;;;;;;;AA2BA,MAAM,gBAAgB,OAAO,cAAyC;CACpE,MAAM,UAAU,MAAM,QAAQ,WAAW,EAAE,eAAe,KAAK,CAAC;CAYhE,QAAO,MAVmB,QAAQ,IAChC,QAAQ,IAAI,OAAO,UAAU;EAC3B,MAAM,YAAY,KAAK,WAAW,MAAM,IAAI;EAE5C,IAAI,MAAM,YAAY,GAAG,OAAO,cAAc,SAAS;EAEvD,OAAO,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS,OAAO,IAAI,CAAC,SAAS,IAAI,CAAC;CACzE,CAAC,CACH,EAEkB,CAAC,KAAK;AAC1B;;;;;;;;AASA,MAAM,eAAe,iBAAyB,aAAgC;CAC5E,MAAM,eAAe,SAAS,iBAAiB,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG;CAE5E,MAAM,mBAAmB,aAAa,SAAS,YAAY;CAM3D,OAAO;EACL;EACA,SAAS,IANkB,mBACzB,aAAa,MAAM,GAAG,GAAoB,CAAC,CAAC,QAAQ,OAAO,EAAE,IAC7D,aAAa,MAAM,GAAG,EAAe,IAIH,QAAQ,WAAW,GAAG;EAC1D;CACF;AACF;;;;AAKA,MAAM,qBACJ,SACA,YACwD;CACxD,MAAM,eAAe,QAAQ,MAAM,GAAG,CAAC,CAAC;CAExC,IAAI,gBAAgB,QAAQ,SAAS,YAAsB,GACzD,OAAO;EACL,cAAc,IAAI;EAClB,mBAAmB,QAAQ,MAAM,aAAa,SAAS,CAAC,KAAK;CAC/D;CAGF,OAAO;EAAE,cAAc;EAAI,mBAAmB,WAAW;CAAI;AAC/D;;;;;AAMA,MAAM,cACJ,iBACA,SACA,qBACW;CACX,MAAM,cAAc,QAAQ,QAAQ,OAAO,EAAE;CAE7C,OAAO,KACL,iBACA,mBAAmB,KAAK,aAAa,YAAY,IAAI,GAAG,YAAY,MACtE;AACF;;;;;;;;;;;;;;;;;;AAmBA,MAAa,qBAAqB,OAChC,eACA,uBAC0C;CAC1C,MAAM,EAAE,SAAS,yBAAyB;CAE1C,MAAM,eAAe,gBAAgB,QAAQ,SAAS,KAAK;CAI3D,MAAM,eACJ,QAAQ,SAAS,gBAAgB,QAAQ,SAAS;CAEpD,IAAI,CAAC,gBAAgB,CAAC,cAAc,OAAO,CAAC;CAE5C,MAAM,UAAU,qBAAqB;CACrC,MAAM,gBAAgB,qBAAqB;CAE3C,MAAM,kBAAkB,cAAc,kBAAkB;CACxD,MAAM,YAAY,MAAM,cAAc,eAAe;CAErD,MAAM,eAA6C,CAAC;CAEpD,KAAK,MAAM,YAAY,WAAW;EAChC,MAAM,EAAE,UAAU,SAAS,qBAAqB,YAC9C,iBACA,QACF;EAEA,MAAM,EAAE,cAAc,sBAAsB,kBAC1C,SACA,OACF;EAIA,MAAM,SAAU,aAAa,MAAM,CAAC,KAAK;EAEzC,MAAM,gBAAgB,iBACpB,mBACA,QACA,YACF;EAEA,MAAM,EAAE,MAAM,eAAe,gBAAgB,qBAC3C,eACA,QACA,YACF;EAGA,IAAI,CAAC,eAAe,kBAAkB,mBAAmB;EAEzD,MAAM,gBAAgB,GAAG,eAAe,gBAAgB,QACtD,WACA,GACF;EACA,MAAM,iBAAiB,WACrB,iBACA,eACA,gBACF;EAEA,MAAM,MAAM,QAAQ,cAAc,GAAG,EAAE,WAAW,KAAK,CAAC;EACxD,MAAM,SAAS,UAAU,cAAc;EAEvC,aAAa,KAAK,CAAC,SAAS,aAAa,CAAC;CAC5C;CAEA,OAAO;AACT"}
@@ -1,3 +1,4 @@
1
+ import { emitRewrittenPages } from "./emitRewrittenPages.mjs";
1
2
  import { resolve } from "node:path";
2
3
  import { getConfiguration } from "@intlayer/config/node";
3
4
  import { getAlias } from "@intlayer/config/utils";
@@ -14,6 +15,7 @@ import { intlayer as intlayer$1, intlayerProxy } from "vite-intlayer";
14
15
  * 2. Injecting Vite plugins for aliases, locale-based routing (middleware), and build optimizations (prune).
15
16
  * 3. Configuring Vite aliases for dictionary access.
16
17
  * 4. Starting a file watcher for dictionary changes during development.
18
+ * 5. Emitting the prerendered pages at their rewritten (localized) URLs.
17
19
  *
18
20
  * @returns An Astro integration object.
19
21
  *
@@ -45,6 +47,11 @@ const intlayer = () => ({
45
47
  "astro:server:setup": async () => {
46
48
  const configuration = getConfiguration();
47
49
  if (configuration.content.watch) await watch({ configuration });
50
+ },
51
+ "astro:build:done": async ({ dir, logger }) => {
52
+ const configuration = getConfiguration();
53
+ const emittedPages = await emitRewrittenPages(configuration, dir);
54
+ if (emittedPages.length > 0) logger.info(`Emitted ${emittedPages.length} rewritten page(s): ${emittedPages.map(([from, to]) => `${from} \u2192 ${to}`).join(", ")}`);
48
55
  }
49
56
  }
50
57
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["viteIntlayerPlugin","viteIntlayerProxyPlugin"],"sources":["../../src/index.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getAlias } from '@intlayer/config/utils';\nimport { prepareIntlayer } from '@intlayer/engine/build';\nimport { watch } from '@intlayer/engine/watcher';\nimport type { AstroIntegration } from 'astro';\nimport type { PluginOption } from 'vite';\nimport {\n intlayer as viteIntlayerPlugin,\n intlayerProxy as viteIntlayerProxyPlugin,\n} from 'vite-intlayer';\n\n/**\n * Astro integration for Intlayer.\n *\n * It handles:\n * 1. Preparing Intlayer resources (dictionaries) at config setup.\n * 2. Injecting Vite plugins for aliases, locale-based routing (middleware), and build optimizations (prune).\n * 3. Configuring Vite aliases for dictionary access.\n * 4. Starting a file watcher for dictionary changes during development.\n *\n * @returns An Astro integration object.\n *\n * @example\n * ```ts\n * // astro.config.mjs\n * import { defineConfig } from 'astro/config';\n * import { intlayer } from 'astro-intlayer';\n *\n * export default defineConfig({\n * integrations: [intlayer()],\n * });\n * ```\n */\nexport const intlayer = (): AstroIntegration =>\n ({\n name: 'astro-intlayer',\n hooks: {\n 'astro:config:setup': async ({ updateConfig }) => {\n const configuration = getConfiguration();\n\n // Prepare once per process start to ensure generated entries exist\n await prepareIntlayer(configuration);\n\n updateConfig({\n vite: {\n plugins: [\n // Aliases + watcher + buildStart prep\n // (also handles optimize/prune/minify internally)\n viteIntlayerPlugin(),\n // Dev-time middleware for locale routing\n viteIntlayerProxyPlugin(),\n ] as PluginOption[],\n resolve: {\n alias: {\n ...getAlias({\n configuration,\n formatter: (value) => resolve(value),\n }),\n },\n },\n },\n });\n },\n\n 'astro:server:setup': async () => {\n const configuration = getConfiguration();\n\n if (configuration.content.watch) {\n await watch({ configuration });\n }\n },\n },\n }) satisfies AstroIntegration;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,kBACV;CACC,MAAM;CACN,OAAO;EACL,sBAAsB,OAAO,EAAE,mBAAmB;GAChD,MAAM,gBAAgB,iBAAiB;GAGvC,MAAM,gBAAgB,aAAa;GAEnC,aAAa,EACX,MAAM;IACJ,SAAS,CAGPA,WAAmB,GAEnBC,cAAwB,CAC1B;IACA,SAAS,EACP,OAAO,EACL,GAAG,SAAS;KACV;KACA,YAAY,UAAU,QAAQ,KAAK;IACrC,CAAC,EACH,EACF;GACF,EACF,CAAC;EACH;EAEA,sBAAsB,YAAY;GAChC,MAAM,gBAAgB,iBAAiB;GAEvC,IAAI,cAAc,QAAQ,OACxB,MAAM,MAAM,EAAE,cAAc,CAAC;EAEjC;CACF;AACF"}
1
+ {"version":3,"file":"index.mjs","names":["viteIntlayerPlugin","viteIntlayerProxyPlugin"],"sources":["../../src/index.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getAlias } from '@intlayer/config/utils';\nimport { prepareIntlayer } from '@intlayer/engine/build';\nimport { watch } from '@intlayer/engine/watcher';\nimport type { AstroIntegration } from 'astro';\nimport type { PluginOption } from 'vite';\nimport {\n intlayer as viteIntlayerPlugin,\n intlayerProxy as viteIntlayerProxyPlugin,\n} from 'vite-intlayer';\nimport { emitRewrittenPages } from './emitRewrittenPages';\n\n/**\n * Astro integration for Intlayer.\n *\n * It handles:\n * 1. Preparing Intlayer resources (dictionaries) at config setup.\n * 2. Injecting Vite plugins for aliases, locale-based routing (middleware), and build optimizations (prune).\n * 3. Configuring Vite aliases for dictionary access.\n * 4. Starting a file watcher for dictionary changes during development.\n * 5. Emitting the prerendered pages at their rewritten (localized) URLs.\n *\n * @returns An Astro integration object.\n *\n * @example\n * ```ts\n * // astro.config.mjs\n * import { defineConfig } from 'astro/config';\n * import { intlayer } from 'astro-intlayer';\n *\n * export default defineConfig({\n * integrations: [intlayer()],\n * });\n * ```\n */\nexport const intlayer = (): AstroIntegration =>\n ({\n name: 'astro-intlayer',\n hooks: {\n 'astro:config:setup': async ({ updateConfig }) => {\n const configuration = getConfiguration();\n\n // Prepare once per process start to ensure generated entries exist\n await prepareIntlayer(configuration);\n\n updateConfig({\n vite: {\n plugins: [\n // Aliases + watcher + buildStart prep\n // (also handles optimize/prune/minify internally)\n viteIntlayerPlugin(),\n // Dev-time middleware for locale routing\n viteIntlayerProxyPlugin(),\n ] as PluginOption[],\n resolve: {\n alias: {\n ...getAlias({\n configuration,\n formatter: (value) => resolve(value),\n }),\n },\n },\n },\n });\n },\n\n 'astro:server:setup': async () => {\n const configuration = getConfiguration();\n\n if (configuration.content.watch) {\n await watch({ configuration });\n }\n },\n\n // Astro renders each page from its canonical file-system route, so a\n // static build has no file for the localized paths declared in\n // `routing.rewrite`. Mirror them here, otherwise the URLs produced by\n // `getLocalizedUrl` (links, hreflang, sitemap) 404 once deployed.\n 'astro:build:done': async ({ dir, logger }) => {\n const configuration = getConfiguration();\n\n const emittedPages = await emitRewrittenPages(configuration, dir);\n\n if (emittedPages.length > 0) {\n logger.info(\n `Emitted ${emittedPages.length} rewritten page(s): ${emittedPages\n .map(([from, to]) => `${from} \\u2192 ${to}`)\n .join(', ')}`\n );\n }\n },\n },\n }) satisfies AstroIntegration;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,MAAa,kBACV;CACC,MAAM;CACN,OAAO;EACL,sBAAsB,OAAO,EAAE,mBAAmB;GAChD,MAAM,gBAAgB,iBAAiB;GAGvC,MAAM,gBAAgB,aAAa;GAEnC,aAAa,EACX,MAAM;IACJ,SAAS,CAGPA,WAAmB,GAEnBC,cAAwB,CAC1B;IACA,SAAS,EACP,OAAO,EACL,GAAG,SAAS;KACV;KACA,YAAY,UAAU,QAAQ,KAAK;IACrC,CAAC,EACH,EACF;GACF,EACF,CAAC;EACH;EAEA,sBAAsB,YAAY;GAChC,MAAM,gBAAgB,iBAAiB;GAEvC,IAAI,cAAc,QAAQ,OACxB,MAAM,MAAM,EAAE,cAAc,CAAC;EAEjC;EAMA,oBAAoB,OAAO,EAAE,KAAK,aAAa;GAC7C,MAAM,gBAAgB,iBAAiB;GAEvC,MAAM,eAAe,MAAM,mBAAmB,eAAe,GAAG;GAEhE,IAAI,aAAa,SAAS,GACxB,OAAO,KACL,WAAW,aAAa,OAAO,sBAAsB,aAClD,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,UAAU,IAAI,CAAC,CAC3C,KAAK,IAAI,GACd;EAEJ;CACF;AACF"}
@@ -0,0 +1,23 @@
1
+ import { IntlayerConfig } from "@intlayer/types/config";
2
+ //#region src/emitRewrittenPages.d.ts
3
+ /**
4
+ * Emits a copy of every prerendered page at its rewritten ("pretty") URL.
5
+ *
6
+ * Astro renders pages from their canonical file-system route (`/about`,
7
+ * `/en/about`), so a static build contains no file for the localized paths
8
+ * declared in `routing.rewrite` (`/nosotros`). The dev and SSR proxies resolve
9
+ * those paths at request time, but a static host has nothing to serve and
10
+ * answers 404 — even though `getLocalizedUrl` (links, hreflang, sitemap)
11
+ * already points at them.
12
+ *
13
+ * This mirrors each canonical page onto its localized path at the end of the
14
+ * build. The canonical path is kept reachable, matching the proxy behaviour.
15
+ *
16
+ * @param configuration - The resolved Intlayer configuration.
17
+ * @param outputDirectoryUrl - The build output directory, as given by `astro:build:done`.
18
+ * @returns The list of `[from, to]` URL paths that were emitted.
19
+ */
20
+ declare const emitRewrittenPages: (configuration: IntlayerConfig, outputDirectoryUrl: URL) => Promise<[from: string, to: string][]>;
21
+ //#endregion
22
+ export { emitRewrittenPages };
23
+ //# sourceMappingURL=emitRewrittenPages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emitRewrittenPages.d.ts","names":[],"sources":["../../src/emitRewrittenPages.ts"],"mappings":";;;;;;;;;;;;;;;;;;;cAuHa,qBAAkB,eACd,gBAAc,oBACT,QACnB,SAAS,cAAc"}
@@ -8,6 +8,7 @@ import { AstroIntegration } from "astro";
8
8
  * 2. Injecting Vite plugins for aliases, locale-based routing (middleware), and build optimizations (prune).
9
9
  * 3. Configuring Vite aliases for dictionary access.
10
10
  * 4. Starting a file watcher for dictionary changes during development.
11
+ * 5. Emitting the prerendered pages at their rewritten (localized) URLs.
11
12
  *
12
13
  * @returns An Astro integration object.
13
14
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cAkCa,gBAAe"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;cAoCa,gBAAe"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-intlayer",
3
- "version": "9.3.1",
3
+ "version": "9.3.3",
4
4
  "private": false,
5
5
  "description": "Easily internationalize i18n your Astro applications with type-safe multilingual content management.",
6
6
  "keywords": [
@@ -78,11 +78,11 @@
78
78
  "typecheck": "tsc --noEmit --project tsconfig.types.json"
79
79
  },
80
80
  "dependencies": {
81
- "@intlayer/config": "9.3.1",
82
- "@intlayer/core": "9.3.1",
83
- "@intlayer/engine": "9.3.1",
84
- "@intlayer/types": "9.3.1",
85
- "vite-intlayer": "9.3.1"
81
+ "@intlayer/config": "9.3.3",
82
+ "@intlayer/core": "9.3.3",
83
+ "@intlayer/engine": "9.3.2",
84
+ "@intlayer/types": "9.3.3",
85
+ "vite-intlayer": "9.3.3"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@types/node": "26.2.0",
@@ -93,7 +93,7 @@
93
93
  "rimraf": "6.1.3",
94
94
  "tsdown": "0.22.14",
95
95
  "typescript": "7.0.2",
96
- "vitest": "4.1.10"
96
+ "vitest": "4.1.11"
97
97
  },
98
98
  "peerDependencies": {
99
99
  "astro": ">=4.0.0",