@lexibase/rdx-next 3.2.0 → 4.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.
@@ -1,7 +1,29 @@
1
1
  import ts from 'typescript'
2
2
 
3
3
  export const versioningPackage = '@lexibase/rdx-versioning'
4
- const manual = `Cannot safely edit rdx.config.ts. Add "import { versioning } from '${versioningPackage}'" and versioning() to plugins manually, then run rdx prepare.`
4
+ export const i18nPackage = '@lexibase/rdx-i18n'
5
+ const versioning = {
6
+ packageName: versioningPackage,
7
+ name: 'versioning',
8
+ label: 'Versioning',
9
+ }
10
+ const localization = { packageName: i18nPackage, name: 'i18n', label: 'i18n' }
11
+
12
+ export function activateVersioning(source) {
13
+ return editPluginConfig(source, versioning)
14
+ }
15
+
16
+ export function inspectI18n(source) {
17
+ return editPluginConfig(source, localization, '', true)
18
+ }
19
+
20
+ export function activateI18n(source, locales) {
21
+ return editPluginConfig(
22
+ source,
23
+ localization,
24
+ JSON.stringify({ locales }, null, 2)
25
+ )
26
+ }
5
27
 
6
28
  function unwrap(node) {
7
29
  while (
@@ -14,7 +36,9 @@ function unwrap(node) {
14
36
  }
15
37
 
16
38
  // Edit only known literal shapes; never print/reconstruct the user's entire file.
17
- export function activateVersioning(source) {
39
+ function editPluginConfig(source, plugin, argument = '', inspect = false) {
40
+ const { packageName, name: pluginName, label } = plugin
41
+ const manual = `Cannot safely edit rdx.config.ts. Add "import { ${pluginName} } from '${packageName}'" and ${pluginName}(${pluginName === 'i18n' ? '{ locales: [...] }' : ''}) to plugins manually, then run rdx prepare.`
18
42
  const file = ts.createSourceFile(
19
43
  'rdx.config.ts',
20
44
  source,
@@ -44,7 +68,7 @@ export function activateVersioning(source) {
44
68
  imported === 'defineConfig'
45
69
  )
46
70
  definitions.add(specifier.name.text)
47
- if (name === versioningPackage && imported === 'versioning') {
71
+ if (name === packageName && imported === pluginName) {
48
72
  if (factory) throw new Error(manual)
49
73
  factory = specifier.name.text
50
74
  }
@@ -52,10 +76,10 @@ export function activateVersioning(source) {
52
76
  } else if (
53
77
  bindings &&
54
78
  ts.isNamespaceImport(bindings) &&
55
- name === versioningPackage
79
+ name === packageName
56
80
  ) {
57
81
  if (factory) throw new Error(manual)
58
- factory = `${bindings.name.text}.versioning`
82
+ factory = `${bindings.name.text}.${pluginName}`
59
83
  }
60
84
  }
61
85
  const exports = file.statements.filter(ts.isExportAssignment)
@@ -100,9 +124,18 @@ export function activateVersioning(source) {
100
124
  )
101
125
  if (matches.length > 1)
102
126
  throw new Error(
103
- 'Versioning is configured more than once. Remove the duplicate plugin before retrying.'
127
+ `${label} is configured more than once. Remove the duplicate plugin before retrying.`
104
128
  )
105
- if (matches.length === 1) return source
129
+ if (matches.length === 1) return inspect ? { configured: true } : source
130
+ }
131
+ if (inspect) {
132
+ const languages = properties.filter((entry) => entry.name.text === 'lang')
133
+ if (languages.length > 1) throw new Error(manual)
134
+ const language = languages.length
135
+ ? unwrap(languages[0].initializer)
136
+ : undefined
137
+ if (language && !ts.isStringLiteral(language)) throw new Error(manual)
138
+ return { configured: false, lang: language?.text ?? 'en' }
106
139
  }
107
140
  const newline = source.includes('\r\n') ? '\r\n' : '\n'
108
141
  const edits = []
@@ -113,11 +146,12 @@ export function activateVersioning(source) {
113
146
  ts.forEachChild(node, visit)
114
147
  }
115
148
  visit(file)
116
- factory = 'versioning'
149
+ factory = pluginName
117
150
  let suffix = 1
118
- while (identifiers.has(factory)) factory = `rdxVersioning${suffix++}`
151
+ const alias = `rdx${pluginName[0].toUpperCase()}${pluginName.slice(1)}`
152
+ while (identifiers.has(factory)) factory = `${alias}${suffix++}`
119
153
  const specifier =
120
- factory === 'versioning' ? 'versioning' : `versioning as ${factory}`
154
+ factory === pluginName ? pluginName : `${pluginName} as ${factory}`
121
155
  let position = imports.length
122
156
  ? imports.at(-1).end
123
157
  : file.statements[0].getStart(file)
@@ -133,7 +167,7 @@ export function activateVersioning(source) {
133
167
  }
134
168
  edits.push({
135
169
  position,
136
- text: `${newline}import { ${specifier} } from '${versioningPackage}'${newline}`,
170
+ text: `${newline}import { ${specifier} } from '${packageName}'${newline}`,
137
171
  })
138
172
  }
139
173
  if (plugins) {
@@ -142,14 +176,14 @@ export function activateVersioning(source) {
142
176
  : plugins.getStart(file) + 1
143
177
  edits.push({
144
178
  position,
145
- text: `${plugins.elements.length ? ',' : ''} ${factory}()`,
179
+ text: `${plugins.elements.length ? ',' : ''} ${factory}(${argument.replaceAll('\n', newline)})`,
146
180
  })
147
181
  } else {
148
182
  if (properties.length && !properties.hasTrailingComma)
149
183
  edits.push({ position: properties.at(-1).end, text: ',' })
150
184
  edits.push({
151
185
  position: config.end - 1,
152
- text: `${newline} plugins: [${factory}()],${newline}`,
186
+ text: `${newline} plugins: [${factory}(${argument.replaceAll('\n', newline)})],${newline}`,
153
187
  })
154
188
  }
155
189
  for (const edit of edits.sort((a, b) => b.position - a.position))
package/config.mjs CHANGED
@@ -2,7 +2,11 @@ import { readdir, realpath, stat } from 'node:fs/promises'
2
2
  import { createRequire } from 'node:module'
3
3
  import path from 'node:path'
4
4
 
5
- import { createRdxProject, resolveProjectPath } from '@lexibase/rdx-core/config'
5
+ import {
6
+ createRdxProject,
7
+ resolveProjectPath,
8
+ getContentSources,
9
+ } from '@lexibase/rdx-core/config'
6
10
  import { createJiti } from 'jiti'
7
11
 
8
12
  /** Include the project and the real Next.js installation in Turbopack's root. */
@@ -77,7 +81,7 @@ export async function getRdxTracingConfig({ rootDir, appDir = rootDir }) {
77
81
  const project = await createRdxProject(input, { rootDir })
78
82
  const files = (
79
83
  await Promise.all(
80
- project.sources
84
+ getContentSources(project)
81
85
  .filter((source) => source.redirectTo === undefined)
82
86
  .map((source) =>
83
87
  contentFiles(
@@ -102,11 +106,28 @@ export async function getRdxTracingConfig({ rootDir, appDir = rootDir }) {
102
106
  }
103
107
  const redirects = project.sources
104
108
  .filter((source) => source.redirectTo !== undefined)
105
- .map((source) => ({
106
- source: `/docs/${source.routePrefix}/:path*`,
107
- destination: `/docs/${source.redirectTo}/:path*`,
109
+ .flatMap((source) =>
110
+ [
111
+ '/docs',
112
+ ...(project.localization?.locales.map(
113
+ (locale) => `/${locale.path}/docs`
114
+ ) ?? []),
115
+ ].map((base) => ({
116
+ source: `${base}/${source.routePrefix}/:path*`,
117
+ destination: `${base}/${source.redirectTo}/:path*`,
118
+ permanent: true,
119
+ }))
120
+ )
121
+ if (project.localization) {
122
+ const primary = project.localization.locales.find(
123
+ (locale) => locale.code === project.localization.defaultLocale
124
+ )
125
+ redirects.push({
126
+ source: '/docs/:path*',
127
+ destination: `/${primary.path}/docs/:path*`,
108
128
  permanent: true,
109
- }))
129
+ })
130
+ }
110
131
  return {
111
132
  ...(redirects.length ? { redirects: async () => redirects } : {}),
112
133
  outputFileTracingIncludes: {
@@ -1,14 +1,26 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { RdxProject, RDXConfig, RdxDocPage } from '@lexibase/rdx-types';
4
- export { createRdxProject, getProjectDocsPaths } from '@lexibase/rdx-core';
5
- import { Metadata } from 'next';
3
+ import { RdxProject, RDXConfig, RdxContentSource, RdxDocPage } from '@lexibase/rdx-types';
4
+ export { createRdxProject, getLocalizedProject, getProjectDocsPaths } from '@lexibase/rdx-core';
5
+ import { MetadataRoute, Metadata } from 'next';
6
6
 
7
7
  declare function getSearchSources(project: RdxProject): {
8
8
  source: string;
9
9
  }[];
10
10
  declare function getSearchResponse(project: RdxProject, key: string): Response;
11
11
 
12
+ /** Published editions replace live development content in search results. */
13
+ declare function getSourceRobots(project: RdxProject, source: RdxContentSource): {
14
+ index: boolean;
15
+ follow: boolean;
16
+ };
17
+ /** Use the configured public URL only; development hosts are never inferred. */
18
+ declare function getRdxRobots(config: RDXConfig): MetadataRoute.Robots;
19
+ /** Share the indexing policy while allowing host applications to map their URLs. */
20
+ declare function getRdxSitemap(project: RdxProject, { documentPath, }?: {
21
+ documentPath?: (source: RdxContentSource, page: RdxDocPage) => string;
22
+ }): MetadataRoute.Sitemap;
23
+
12
24
  /** Read metadata from the same content source and index used by the page. */
13
25
  declare function getDocumentMetadata({ project, slug, }: {
14
26
  project: RdxProject;
@@ -35,4 +47,4 @@ declare function DocsPage({ slug, project, }: {
35
47
  project: RdxProject;
36
48
  }): Promise<react.JSX.Element>;
37
49
 
38
- export { DocsPage, HomePage, NotFoundPage, RdxLayout, getDocumentMetadata, getPageMetadata, getRdxMetadata, getSearchResponse, getSearchSources };
50
+ export { DocsPage, HomePage, NotFoundPage, RdxLayout, getDocumentMetadata, getPageMetadata, getRdxMetadata, getRdxRobots, getRdxSitemap, getSearchResponse, getSearchSources, getSourceRobots };
@@ -1,11 +1,17 @@
1
1
  // src/runtime/index.tsx
2
2
  import {
3
- getDocument as getDocument2,
3
+ getDocument as getDocument3,
4
4
  getDocsPaths,
5
- getSourceOptions as getSourceOptions2,
6
- resolveDocsRoute as resolveDocsRoute2
5
+ getSourceOptions as getSourceOptions4,
6
+ resolveDocsRoute as resolveDocsRoute2,
7
+ getLocalizedProject as getLocalizedProject2,
8
+ getDocsHref as getDocsHref3,
9
+ getLanguageLinks,
10
+ isFallbackPage as isFallbackPage3
7
11
  } from "@lexibase/rdx-core";
8
12
  import { Button } from "@lexibase/rdx-ui/components/button";
13
+ import { Callout } from "@lexibase/rdx-ui/components/callout";
14
+ import { LanguageDropdown } from "@lexibase/rdx-ui/components/language-dropdown";
9
15
  import { DocsSidebar } from "@lexibase/rdx-ui/components/docs-sidebar";
10
16
  import { DocsToc } from "@lexibase/rdx-ui/components/docs-toc";
11
17
  import { DocsPageTransition } from "@lexibase/rdx-ui/components/docs-page-transition";
@@ -19,6 +25,7 @@ import { getMessages, formatMessage } from "@lexibase/rdx-ui/lib/i18n";
19
25
  import { LocaleProvider } from "@lexibase/rdx-ui/providers/locale-provider";
20
26
  import { Navbar } from "@lexibase/rdx-ui/components/navbar";
21
27
  import { NotFound } from "@lexibase/rdx-ui/components/not-found";
28
+ import { LocalizedNotFound } from "@lexibase/rdx-ui/components/localized-not-found";
22
29
  import {
23
30
  VersionDropdown
24
31
  } from "@lexibase/rdx-ui/components/version-dropdown";
@@ -54,50 +61,156 @@ function compileDocumentation(source, components, links) {
54
61
  }
55
62
 
56
63
  // src/runtime/index.tsx
57
- import { createRdxProject, getProjectDocsPaths } from "@lexibase/rdx-core";
64
+ import {
65
+ createRdxProject,
66
+ getProjectDocsPaths,
67
+ getLocalizedProject as getLocalizedProject3
68
+ } from "@lexibase/rdx-core";
58
69
 
59
70
  // src/runtime/search.ts
60
- import { getDocsIndex, getDocument, getSourceOptions } from "@lexibase/rdx-core";
71
+ import {
72
+ getDocsIndex,
73
+ getDocument,
74
+ getSourceOptions,
75
+ getDocsHref
76
+ } from "@lexibase/rdx-core";
61
77
  import { buildSearchIndex } from "@lexibase/rdx-core/search";
62
78
  function getSearchSources(project) {
63
79
  return project.config.search ? project.sources.filter((source) => source.redirectTo === void 0).map((source) => ({ source: source.routePrefix || "_" })) : [];
64
80
  }
65
81
  function getSearchResponse(project, key) {
66
- if (!project.config.search) return new Response(null, { status: 404 });
82
+ const headers = { "X-Robots-Tag": "noindex, follow" };
83
+ if (!project.config.search)
84
+ return new Response(null, { status: 404, headers });
67
85
  const source = project.sources.find(
68
86
  (entry) => entry.redirectTo === void 0 && (entry.routePrefix || "_") === key
69
87
  );
70
- if (!source) return new Response(null, { status: 404 });
71
- const options = getSourceOptions(project, source);
88
+ if (!source) return new Response(null, { status: 404, headers });
89
+ if (source.fallbackOnly)
90
+ return Response.json(buildSearchIndex([]), { headers });
91
+ const { docsDir } = getSourceOptions(project, source);
92
+ const options = { docsDir };
72
93
  const documents = getDocsIndex(options).flatMap(
73
94
  (category) => category.pages.map((page) => ({
74
95
  title: page.title,
75
96
  description: page.description,
76
97
  category: category.title,
77
- href: [
78
- "/docs",
79
- source.routePrefix,
80
- page.slug.split("/").map(encodeURIComponent).join("/")
81
- ].filter(Boolean).join("/"),
98
+ href: getDocsHref(project, source, page.slug),
82
99
  content: getDocument(page.slug, options)?.content ?? ""
83
100
  }))
84
101
  );
85
- return Response.json(buildSearchIndex(documents));
102
+ return Response.json(buildSearchIndex(documents), { headers });
103
+ }
104
+
105
+ // src/runtime/indexing.ts
106
+ import {
107
+ getDocsIndex as getDocsIndex2,
108
+ getSourceOptions as getSourceOptions2,
109
+ getContentSources,
110
+ isFallbackPage
111
+ } from "@lexibase/rdx-core";
112
+ function getSourceRobots(project, source) {
113
+ const published = project.sources.some(
114
+ (entry) => entry.id !== "current" && entry.redirectTo === void 0 && entry.badge !== "Archived"
115
+ );
116
+ return {
117
+ index: source.redirectTo === void 0 && source.badge !== "Archived" && (source.id !== "current" || !published),
118
+ follow: true
119
+ };
120
+ }
121
+ function getRdxRobots(config) {
122
+ return {
123
+ // Do not disallow noindex pages: crawlers must read their HTML directive.
124
+ rules: { userAgent: "*", allow: "/" },
125
+ ...config.metadata.metadataBase ? { sitemap: new URL("/sitemap.xml", config.metadata.metadataBase).href } : {}
126
+ };
127
+ }
128
+ function getRdxSitemap(project, {
129
+ documentPath = (source, page) => [
130
+ source.basePath ?? "/docs",
131
+ source.routePrefix,
132
+ page.slug.split("/").map(encodeURIComponent).join("/")
133
+ ].filter(Boolean).join("/")
134
+ } = {}) {
135
+ const base = project.config.metadata.metadataBase;
136
+ if (!base) return [];
137
+ const urls = /* @__PURE__ */ new Set();
138
+ if (project.config.homepage?.enabled) urls.add(new URL("/", base).href);
139
+ for (const source of getContentSources(project)) {
140
+ if (!getSourceRobots(project, source).index) continue;
141
+ for (const category of getDocsIndex2(getSourceOptions2(project, source))) {
142
+ for (const page of category.pages) {
143
+ if (isFallbackPage(project, source, page.sourcePath)) continue;
144
+ urls.add(new URL(documentPath(source, page), base).href);
145
+ }
146
+ }
147
+ }
148
+ return [...urls].map((url) => ({ url }));
86
149
  }
87
150
 
88
151
  // src/runtime/metadata.ts
89
- import { resolveDocsRoute } from "@lexibase/rdx-core";
152
+ import {
153
+ resolveDocsRoute,
154
+ getLocalizedProject,
155
+ getDocsHref as getDocsHref2,
156
+ isFallbackPage as isFallbackPage2,
157
+ getDocument as getDocument2,
158
+ getSourceOptions as getSourceOptions3
159
+ } from "@lexibase/rdx-core";
90
160
  import { notFound } from "next/navigation";
91
161
  function getDocumentMetadata({
92
162
  project,
93
163
  slug
94
164
  }) {
165
+ if (project.localization && !project.locale)
166
+ return { robots: { index: false, follow: true } };
95
167
  const route = resolveDocsRoute(project, slug);
96
- if (route.source.redirectTo !== void 0) return {};
97
- if (!route.slug) return {};
168
+ if (route.source.redirectTo !== void 0)
169
+ return { robots: getSourceRobots(project, route.source) };
170
+ if (!route.slug) return { robots: { index: false, follow: true } };
98
171
  if (!route.document) notFound();
99
172
  const page = route.categories.flatMap((category) => category.pages).find((page2) => page2.slug === route.slug);
100
- return getPageMetadata(project.config, page);
173
+ const fallback = isFallbackPage2(project, route.source, page.sourcePath);
174
+ const primary = project.localization?.locales.find(
175
+ (locale) => locale.code === project.localization.defaultLocale
176
+ );
177
+ const canonicalProject = fallback ? getLocalizedProject(project, primary.path) : project;
178
+ const base = project.config.metadata.metadataBase;
179
+ const languages = project.localization && base ? Object.fromEntries(
180
+ project.localization.locales.flatMap((locale) => {
181
+ const view = getLocalizedProject(project, locale.path);
182
+ const source = view.sources.find(
183
+ (entry) => entry.id === route.source.id
184
+ );
185
+ return !source.fallbackOnly && getDocument2(page.slug, {
186
+ docsDir: getSourceOptions3(view, source).docsDir
187
+ }) ? [
188
+ [
189
+ locale.code,
190
+ new URL(getDocsHref2(view, source, page.slug), base).href
191
+ ]
192
+ ] : [];
193
+ })
194
+ ) : void 0;
195
+ return {
196
+ ...getPageMetadata(project.config, page),
197
+ robots: fallback ? { index: false, follow: true } : getSourceRobots(project, route.source),
198
+ ...project.localization && base ? {
199
+ alternates: {
200
+ canonical: new URL(
201
+ getDocsHref2(
202
+ canonicalProject,
203
+ canonicalProject.sources.find(
204
+ (entry) => entry.id === route.source.id
205
+ ),
206
+ page.slug
207
+ ),
208
+ base
209
+ ).href,
210
+ ...fallback ? {} : { languages }
211
+ }
212
+ } : {}
213
+ };
101
214
  }
102
215
  function getPageMetadata(config, page) {
103
216
  const title = page.title;
@@ -117,6 +230,7 @@ function getRdxMetadata(config) {
117
230
  title: { default: name, template: `%s | ${name}` },
118
231
  description: config.description,
119
232
  ...metadata,
233
+ robots: { index: true, follow: true },
120
234
  ...metadataBase ? { metadataBase: new URL(metadataBase) } : {},
121
235
  ...metadata.openGraph ? {
122
236
  openGraph: {
@@ -164,13 +278,13 @@ function RdxHeader({
164
278
  const messages = getMessages(config.lang);
165
279
  const { source, slug: currentSlug } = resolveDocsRoute2(project, slug);
166
280
  const options = project.sources.filter((edition) => edition.redirectTo === void 0).map((edition) => {
167
- const docs = getSourceOptions2(project, edition);
168
- const destination = currentSlug && getDocument2(currentSlug, docs) ? currentSlug : getDocsPaths(docs)[0];
281
+ const docs = getSourceOptions4(project, edition);
282
+ const destination = currentSlug && getDocument3(currentSlug, docs) ? currentSlug : getDocsPaths(docs)[0];
169
283
  return {
170
284
  id: edition.id,
171
285
  label: edition.id === "current" ? messages.current : formatMessage(messages.version, { version: edition.label }),
172
286
  kind: edition.id === "current" ? "live" : edition.badge === "Archived" ? "archived" : "version",
173
- href: ["/docs", edition.routePrefix, destination].filter(Boolean).join("/")
287
+ href: getDocsHref3(project, edition, destination)
174
288
  };
175
289
  });
176
290
  const BrandLogo = config.navbar?.logo ?? Logo;
@@ -182,14 +296,23 @@ function RdxHeader({
182
296
  search: config.search ? /* @__PURE__ */ jsx(
183
297
  DocsSearch,
184
298
  {
185
- indexUrl: `/rdx-search/${encodeURIComponent(source.routePrefix || "_")}`,
299
+ indexUrl: `${project.locale ? `/${project.locale.path}` : ""}/rdx-search/${encodeURIComponent(source.routePrefix || "_")}`,
186
300
  version: source.id === "current" ? messages.current : formatMessage(messages.version, { version: source.label })
187
301
  }
188
302
  ) : void 0,
189
303
  siteTitle: config.metadata.applicationName,
190
304
  logo: /* @__PURE__ */ jsx(BrandLogo, { className: "size-5" }),
191
- homeHref: config.homepage?.enabled ? "/" : "/docs",
192
- docsHref: ["/docs", source.routePrefix].filter(Boolean).join("/"),
305
+ homeHref: config.homepage?.enabled ? "/" : getDocsHref3(project),
306
+ docsHref: getDocsHref3(project, source),
307
+ languageMenu: project.localization ? /* @__PURE__ */ jsx(
308
+ LanguageDropdown,
309
+ {
310
+ lang: config.lang,
311
+ current: project.locale?.code ?? project.localization.defaultLocale,
312
+ options: getLanguageLinks(project, source, currentSlug),
313
+ contributionUrl: project.localization.contributionUrl
314
+ }
315
+ ) : void 0,
193
316
  versionMenu: project.sources.length > 1 ? /* @__PURE__ */ jsx(VersionDropdown, { options, currentId: source.id }) : void 0,
194
317
  github: config.navbar?.actions?.github,
195
318
  showToggleMode: config.navbar?.actions?.showToggleMode
@@ -198,7 +321,7 @@ function RdxHeader({
198
321
  }
199
322
  function HomePage({ project }) {
200
323
  const { config } = project;
201
- if (!config.homepage?.enabled) redirect("/docs");
324
+ if (!config.homepage?.enabled) redirect(getDocsHref3(project));
202
325
  const homepage = config.homepage;
203
326
  const CustomHome = homepage.component;
204
327
  const BrandLogo = config.navbar?.logo ?? Logo;
@@ -224,6 +347,25 @@ function HomePage({ project }) {
224
347
  ] });
225
348
  }
226
349
  function NotFoundPage({ project }) {
350
+ if (project.localization) {
351
+ const { config, localization } = project;
352
+ const BrandLogo = config.navbar?.logo ?? Logo;
353
+ return /* @__PURE__ */ jsx(
354
+ LocalizedNotFound,
355
+ {
356
+ locales: localization.locales,
357
+ defaultLocale: localization.defaultLocale,
358
+ homepage: !!config.homepage?.enabled,
359
+ brand: {
360
+ title: config.navbar?.title,
361
+ siteTitle: config.metadata.applicationName,
362
+ github: config.navbar?.actions?.github,
363
+ showToggleMode: config.navbar?.actions?.showToggleMode,
364
+ logo: /* @__PURE__ */ jsx(BrandLogo, { className: "size-5" })
365
+ }
366
+ }
367
+ );
368
+ }
227
369
  return /* @__PURE__ */ jsxs(Fragment, { children: [
228
370
  /* @__PURE__ */ jsx(RdxHeader, { project }),
229
371
  /* @__PURE__ */ jsx(NotFound, { lang: project.config.lang })
@@ -233,6 +375,8 @@ async function DocsPage({
233
375
  slug,
234
376
  project
235
377
  }) {
378
+ if (project.localization && !project.locale)
379
+ permanentRedirect(getDocsHref3(project, project.sources[0], slug?.join("/")));
236
380
  const messages = getMessages(project.config.lang);
237
381
  const {
238
382
  source,
@@ -242,11 +386,13 @@ async function DocsPage({
242
386
  } = resolveDocsRoute2(project, slug);
243
387
  if (source.redirectTo !== void 0)
244
388
  permanentRedirect(
245
- [
246
- "/docs",
247
- source.redirectTo,
248
- ...slug?.slice(1).map(encodeURIComponent) ?? []
249
- ].join("/")
389
+ getDocsHref3(
390
+ project,
391
+ project.sources.find(
392
+ (entry) => entry.routePrefix === source.redirectTo
393
+ ),
394
+ slug?.slice(1).join("/")
395
+ )
250
396
  );
251
397
  if (!currentSlug)
252
398
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -257,19 +403,32 @@ async function DocsPage({
257
403
  ] })
258
404
  ] });
259
405
  if (!document) notFound2();
406
+ const fallback = isFallbackPage3(project, source, document.sourcePath);
407
+ const primaryLocale = (source.localization ?? project.localization)?.locales.find(
408
+ (locale) => locale.code === project.localization.defaultLocale
409
+ );
410
+ const originalProject = fallback && primaryLocale ? getLocalizedProject2(project, primaryLocale.path) : project;
411
+ const contentSource = originalProject.sources.find(
412
+ (entry) => entry.id === source.id
413
+ );
414
+ const contentOptions = getSourceOptions4(originalProject, contentSource);
415
+ const contentCategories = fallback ? resolveDocsRoute2(originalProject, slug).categories : categories;
416
+ const contentLanguage = fallback ? primaryLocale.code : project.config.lang;
260
417
  const { content } = await compileDocumentation(
261
418
  document.content,
262
419
  getMdxComponents(project.config.lang),
263
420
  {
264
- sourceRoot: getSourceOptions2(project, source).docsDir,
421
+ sourceRoot: contentOptions.docsDir,
265
422
  sourcePath: document.sourcePath,
266
423
  routePrefix: source.routePrefix,
267
- documents: categories.flatMap((category2) => category2.pages)
424
+ basePath: contentSource.basePath,
425
+ virtualPaths: !!contentOptions.fallbackDocsDir,
426
+ documents: contentCategories.flatMap((category2) => category2.pages)
268
427
  }
269
428
  );
270
429
  const title = typeof document.frontmatter.title === "string" ? document.frontmatter.title : currentSlug;
271
430
  const description = typeof document.frontmatter.description === "string" ? document.frontmatter.description : null;
272
- const hrefFor = (pageSlug) => ["/docs", source.routePrefix, pageSlug].filter(Boolean).join("/");
431
+ const hrefFor = (pageSlug) => getDocsHref3(project, source, pageSlug);
273
432
  const sidebarCategories = categories.map((category2) => ({
274
433
  title: category2.title,
275
434
  links: category2.pages.map((page) => ({
@@ -315,9 +474,58 @@ async function DocsPage({
315
474
  "main",
316
475
  {
317
476
  id: "rdx-main",
477
+ lang: contentLanguage,
318
478
  tabIndex: -1,
319
479
  className: "w-full flex flex-col gap-5 md:mx-auto md:max-w-[860px] pt-14 md:pt-0 flex-1 outline-none",
320
480
  children: [
481
+ fallback && /* @__PURE__ */ jsxs(
482
+ Callout,
483
+ {
484
+ lang: project.config.lang,
485
+ variant: "note",
486
+ title: messages.translationUnavailable,
487
+ "data-rdx-translation-fallback": true,
488
+ children: [
489
+ /* @__PURE__ */ jsx("p", { children: formatMessage(messages.translationFallback, {
490
+ requested: project.locale.label,
491
+ original: primaryLocale.label
492
+ }) }),
493
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-2", children: [
494
+ /* @__PURE__ */ jsx(
495
+ Button,
496
+ {
497
+ variant: "link",
498
+ href: getDocsHref3(
499
+ originalProject,
500
+ contentSource,
501
+ currentSlug
502
+ ),
503
+ children: messages.viewOriginal
504
+ }
505
+ ),
506
+ project.localization?.contributionUrl && /* @__PURE__ */ jsxs(
507
+ Button,
508
+ {
509
+ variant: "link",
510
+ href: project.localization.contributionUrl,
511
+ target: "_blank",
512
+ rel: "noopener noreferrer",
513
+ "data-rdx-translate-page": true,
514
+ children: [
515
+ messages.translateThisPage,
516
+ /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
517
+ " ",
518
+ "(",
519
+ messages.opensInNewTab,
520
+ ")"
521
+ ] })
522
+ ]
523
+ }
524
+ )
525
+ ] })
526
+ ]
527
+ }
528
+ ),
321
529
  /* @__PURE__ */ jsxs("header", { className: "mb-6", children: [
322
530
  source.badge && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: source.badge === "Archived" ? messages.archivedVersion : source.badge }),
323
531
  /* @__PURE__ */ jsx("span", { className: "text-xs text-zinc-600 font-semibold uppercase block -mb-0.5", children: category?.title }),
@@ -378,9 +586,13 @@ export {
378
586
  RdxLayout,
379
587
  createRdxProject,
380
588
  getDocumentMetadata,
589
+ getLocalizedProject3 as getLocalizedProject,
381
590
  getPageMetadata,
382
591
  getProjectDocsPaths,
383
592
  getRdxMetadata,
593
+ getRdxRobots,
594
+ getRdxSitemap,
384
595
  getSearchResponse,
385
- getSearchSources
596
+ getSearchSources,
597
+ getSourceRobots
386
598
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexibase/rdx-next",
3
- "version": "3.2.0",
3
+ "version": "4.1.0",
4
4
  "description": "The Next.js runtime for RDX documentation projects.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,14 +48,15 @@
48
48
  "dependencies": {
49
49
  "cross-spawn": "^7.0.6",
50
50
  "jiti": "^2.6.1",
51
+ "prompts": "^2.4.2",
51
52
  "next-mdx-remote": "^6.0.0",
52
53
  "remark-gfm": "^4",
53
54
  "typescript": "6.0.3",
54
- "@lexibase/rdx-core": "1.4.0",
55
- "@lexibase/rdx-types": "1.2.0"
55
+ "@lexibase/rdx-core": "2.1.0",
56
+ "@lexibase/rdx-types": "2.1.0"
56
57
  },
57
58
  "peerDependencies": {
58
- "@lexibase/rdx-ui": "^3.2.0",
59
+ "@lexibase/rdx-ui": "^3.3.0",
59
60
  "next": "^16.4.2",
60
61
  "react": "^19.3.1",
61
62
  "react-dom": "^19.3.1"
@@ -80,7 +81,7 @@
80
81
  "next": "^16.4.2",
81
82
  "react": "19.3.1",
82
83
  "react-dom": "19.3.1",
83
- "@lexibase/rdx-ui": "3.2.0",
84
+ "@lexibase/rdx-ui": "3.3.0",
84
85
  "@rdx/typescript-config": "1.0.0",
85
86
  "@rdx/eslint-config": "1.0.0"
86
87
  },